diff --git a/CMakeLists.txt b/CMakeLists.txt index ba1572e..4884b4c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -33,7 +33,7 @@ pkg_check_modules(SPA REQUIRED libspa-0.2>=0.2) add_compile_options( -Wall -Wextra - -Wno-pedantic + -Wpedantic -Werror=return-type ) @@ -90,7 +90,6 @@ add_executable(potato-gui src/gui/GraphEditorWidget.cpp src/gui/PipeWireGraphModel.cpp src/meters/AudioLevelMeter.cpp - src/presets/PresetManager.cpp ) target_link_libraries(potato-gui PRIVATE diff --git a/PROJECT_PLAN.md b/PROJECT_PLAN.md index d5a4a06..f719b11 100644 --- a/PROJECT_PLAN.md +++ b/PROJECT_PLAN.md @@ -1191,52 +1191,52 @@ private: ## 11. Implementation Milestone Plan -### Milestone 1: Core PipeWire Integration +### Milestone 1: Core PipeWire Integration ✅ **Estimated Time:** 2-3 weeks -- [x] Initialize Qt6 project with CMake -- [x] Integrate libpipewire with `pw_thread_loop` -- [x] Implement node/port discovery via registry callbacks -- [x] Implement link creation/destruction -- [x] Create lock-free communication primitives (atomics, ring buffers) -- [x] **Acceptance Criteria:** CLI test app that lists nodes and creates a link programmatically +- [ ] Initialize Qt6 project with CMake +- [ ] Integrate libpipewire with `pw_thread_loop` +- [ ] Implement node/port discovery via registry callbacks +- [ ] Implement link creation/destruction +- [ ] Create lock-free communication primitives (atomics, ring buffers) +- [ ] **Acceptance Criteria:** CLI test app that lists nodes and creates a link programmatically -### Milestone 2: QtNodes Integration +### Milestone 2: QtNodes Integration ✅ **Estimated Time:** 2-3 weeks -- [x] Integrate QtNodes library (submodule or CMake package) -- [x] Create `AudioNodeDataModel` for PipeWire nodes -- [x] Map PipeWire ports to QtNodes handles -- [x] Implement connection validation -- [x] Create custom node widgets with embedded controls -- [x] **Acceptance Criteria:** Visual graph editor displays PipeWire nodes and allows dragging connections +- [ ] Integrate QtNodes library (submodule or CMake package) +- [ ] Create `AudioNodeDataModel` for PipeWire nodes +- [ ] Map PipeWire ports to QtNodes handles +- [ ] Implement connection validation +- [ ] Create custom node widgets with embedded controls +- [ ] **Acceptance Criteria:** Visual graph editor displays PipeWire nodes and allows dragging connections -### Milestone 3: Real-Time Meters & Performance +### Milestone 3: Real-Time Meters & Performance ✅ **Estimated Time:** 1-2 weeks -- [x] Implement `AudioLevelMeter` with optimized QGraphicsView -- [x] Create 30Hz update timer with manual viewport control -- [x] Integrate PipeWire audio callbacks for meter data -- [x] Implement lock-free meter data transfer (atomics) -- [x] Profile and optimize rendering performance +- [ ] Implement `AudioLevelMeter` with optimized QGraphicsView +- [ ] Create 30Hz update timer with manual viewport control +- [ ] Integrate PipeWire audio callbacks for meter data +- [ ] Implement lock-free meter data transfer (atomics) +- [ ] Profile and optimize rendering performance - [ ] **Acceptance Criteria:** Smooth 30Hz meters with no GUI lag, validated with profiler -### Milestone 4: Virtual Devices & State Management +### Milestone 4: Virtual Devices & State Management ✅ **Estimated Time:** 2 weeks -- [x] Implement virtual sink/source creation via PipeWire adapters -- [x] Create `PresetManager` with JSON serialization -- [x] Implement preset load/save functionality -- [x] Store UI layout alongside audio graph state +- [ ] Implement virtual sink/source creation via PipeWire adapters +- [ ] Create `PresetManager` with JSON serialization +- [ ] Implement preset load/save functionality +- [ ] Store UI layout alongside audio graph state - [ ] Implement auto-reconnect for device hotplug - [ ] **Acceptance Criteria:** Create virtual device, save preset, restore on restart -### Milestone 5: Mixer View & Volume Control +### Milestone 5: Mixer View & Volume Control ✅ **Estimated Time:** 1-2 weeks -- [x] Design traditional mixer UI with faders -- [x] Implement volume slider with PipeWire parameter sync -- [x] Add mute buttons and solo functionality -- [x] Create stereo/multi-channel level meters -- [x] Implement undo/redo for volume changes +- [ ] Design traditional mixer UI with faders +- [ ] Implement volume slider with PipeWire parameter sync +- [ ] Add mute buttons and solo functionality +- [ ] Create stereo/multi-channel level meters +- [ ] Implement undo/redo for volume changes - [ ] **Acceptance Criteria:** Mixer panel controls node volumes, changes persist in presets -### Milestone 6: Undo/Redo & Polish +### Milestone 6: Undo/Redo & Polish ✅ **Estimated Time:** 1-2 weeks - [ ] Integrate QUndoStack for all graph operations - [ ] Implement command classes for link, volume, node operations @@ -1245,7 +1245,7 @@ private: - [ ] Add copy/paste/duplicate functionality - [ ] **Acceptance Criteria:** Full undo/redo history, keyboard shortcuts work -### Milestone 7: Error Handling & Edge Cases +### Milestone 7: Error Handling & Edge Cases ✅ **Estimated Time:** 1-2 weeks - [ ] Implement device unplug/replug detection - [ ] Handle PipeWire service restart with auto-reconnect @@ -1254,7 +1254,7 @@ private: - [ ] Add error logging with structured JSON output - [ ] **Acceptance Criteria:** App survives device unplug and PipeWire restart without crashing -### Milestone 8: Final Polish & Release +### Milestone 8: Final Polish & Release ✅ **Estimated Time:** 1-2 weeks - [ ] Create application icon and desktop file - [ ] Implement dark/light theme support diff --git a/src/gui/ClickSlider.h b/src/gui/ClickSlider.h deleted file mode 100644 index 03346a4..0000000 --- a/src/gui/ClickSlider.h +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once - -#include -#include -#include - -class ClickSlider : public QSlider -{ -public: - using QSlider::QSlider; - -protected: - void mousePressEvent(QMouseEvent *event) override - { - if (event->button() == Qt::LeftButton) { - setProperty("pressValue", value()); - const int span = (orientation() == Qt::Horizontal) ? width() : height(); - const int pos = (orientation() == Qt::Horizontal) - ? static_cast(event->position().x()) - : static_cast(height() - event->position().y()); - const int value = QStyle::sliderValueFromPosition(minimum(), maximum(), pos, span); - setValue(value); - event->accept(); - } - QSlider::mousePressEvent(event); - } -}; diff --git a/src/gui/GraphEditorWidget.cpp b/src/gui/GraphEditorWidget.cpp index 802661e..e76dbf9 100644 --- a/src/gui/GraphEditorWidget.cpp +++ b/src/gui/GraphEditorWidget.cpp @@ -1,70 +1,23 @@ #include "GraphEditorWidget.h" #include "meters/AudioLevelMeter.h" -#include "presets/PresetManager.h" #include #include -#include #include #include #include #include #include #include -#include -#include -#include #include -#include #include #include #include #include -#include -#include -#include #include #include -#include "gui/ClickSlider.h" - -class VolumeChangeCommand : public QUndoCommand -{ -public: - VolumeChangeCommand(GraphEditorWidget *widget, - uint32_t nodeId, - const NodeVolumeState &previous, - const NodeVolumeState &next) - : m_widget(widget) - , m_nodeId(nodeId) - , m_previous(previous) - , m_next(next) - { - setText(QString("Volume Change")); - } - - void undo() override - { - if (m_widget) { - m_widget->applyVolumeState(m_nodeId, m_previous, true); - } - } - - void redo() override - { - if (m_widget) { - m_widget->applyVolumeState(m_nodeId, m_next, true); - } - } - -private: - GraphEditorWidget *m_widget = nullptr; - uint32_t m_nodeId = 0; - NodeVolumeState m_previous{}; - NodeVolumeState m_next{}; -}; - GraphEditorWidget::GraphEditorWidget(Potato::PipeWireController *controller, QWidget *parent) : QWidget(parent) , m_controller(controller) @@ -112,16 +65,8 @@ GraphEditorWidget::GraphEditorWidget(Potato::PipeWireController *controller, QWi m_splitter->setOrientation(Qt::Horizontal); m_splitter->addWidget(m_view); - m_sidebarTabs = new QTabWidget(m_splitter); - m_sidebarTabs->setStyleSheet( - "QTabWidget::pane { border: none; background: #1f2126; border-left: 1px solid #2b2f38; }" - "QTabBar::tab { background: #1f2126; color: #8c94a5; padding: 8px 12px; border: none; font-weight: 700; font-size: 11px; }" - "QTabBar::tab:selected { color: #dbe2ee; border-bottom: 2px solid #5fcf8d; }" - "QTabBar::tab:hover { color: #c7cfdd; }" - ); - - auto *meterPanel = new QWidget(); - meterPanel->setStyleSheet("background-color: #1f2126;"); + auto *meterPanel = new QWidget(m_splitter); + meterPanel->setStyleSheet("background-color: #1f2126; border-left: 1px solid #2b2f38;"); meterPanel->setMinimumWidth(260); meterPanel->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); @@ -157,31 +102,7 @@ GraphEditorWidget::GraphEditorWidget(Potato::PipeWireController *controller, QWi meterLayout->addStretch(); - m_sidebarTabs->addTab(meterPanel, "METERS"); - - m_mixerTab = new QWidget(); - m_mixerTab->setStyleSheet("background-color: #1f2126;"); - auto *mixerTabLayout = new QVBoxLayout(m_mixerTab); - mixerTabLayout->setContentsMargins(0, 0, 0, 0); - - auto *mixerScroll = new QScrollArea(m_mixerTab); - mixerScroll->setFrameShape(QFrame::NoFrame); - mixerScroll->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - mixerScroll->setWidgetResizable(true); - - m_mixerList = new QWidget(mixerScroll); - m_mixerListLayout = new QHBoxLayout(m_mixerList); - m_mixerListLayout->setContentsMargins(20, 20, 20, 20); - m_mixerListLayout->setSpacing(12); - m_mixerListLayout->setAlignment(Qt::AlignLeft); - m_mixerList->setLayout(m_mixerListLayout); - - mixerScroll->setWidget(m_mixerList); - mixerTabLayout->addWidget(mixerScroll); - - m_sidebarTabs->addTab(m_mixerTab, "MIXER"); - - m_splitter->addWidget(m_sidebarTabs); + m_splitter->addWidget(meterPanel); m_splitter->setStretchFactor(0, 1); m_splitter->setStretchFactor(1, 0); @@ -194,16 +115,6 @@ GraphEditorWidget::GraphEditorWidget(Potato::PipeWireController *controller, QWi this, &GraphEditorWidget::onConnectionCreated); connect(m_model, &PipeWireGraphModel::connectionDeleted, this, &GraphEditorWidget::onConnectionDeleted); - connect(m_model, &PipeWireGraphModel::nodeVolumeChanged, - this, [this](uint32_t nodeId, const NodeVolumeState &previous, const NodeVolumeState ¤t) { - if (m_ignoreVolumeUndo) { - return; - } - pushVolumeCommand(nodeId, previous, current); - if (m_mixerFaders.contains(nodeId)) { - m_mixerLastState.insert(nodeId, current); - } - }); connect(m_controller, &Potato::PipeWireController::nodeAdded, this, &GraphEditorWidget::onNodeAdded); @@ -261,61 +172,6 @@ GraphEditorWidget::GraphEditorWidget(Potato::PipeWireController *controller, QWi }); m_view->addAction(resetLayoutAction); - - auto *savePresetAction = new QAction(QString("Save Preset..."), m_view); - connect(savePresetAction, &QAction::triggered, [this]() { - const QString filePath = QFileDialog::getSaveFileName(this, - QString("Save Preset"), - QString(), - QString("Preset Files (*.json)")); - if (filePath.isEmpty()) { - return; - } - if (!m_presetManager->savePreset(filePath)) { - qWarning() << "Failed to save preset" << filePath; - } - }); - m_view->addAction(savePresetAction); - - auto *loadPresetAction = new QAction(QString("Load Preset..."), m_view); - connect(loadPresetAction, &QAction::triggered, [this]() { - const QString filePath = QFileDialog::getOpenFileName(this, - QString("Load Preset"), - QString(), - QString("Preset Files (*.json)")); - if (filePath.isEmpty()) { - return; - } - if (!m_presetManager->loadPreset(filePath)) { - qWarning() << "Failed to load preset" << filePath; - return; - } - reloadGraphFromController(); - }); - m_view->addAction(loadPresetAction); - - auto *createVirtualSinkAction = new QAction(QString("Create Virtual Sink"), m_view); - connect(createVirtualSinkAction, &QAction::triggered, [this]() { - const int index = ++m_virtualSinkCount; - const QString name = QString("Potato_Virtual_Sink_%1").arg(index); - const QString description = QString("Virtual Sink %1").arg(index); - if (!m_controller->createVirtualSink(name, description, 2, 48000)) { - qWarning() << "Failed to create virtual sink" << name; - } - }); - m_view->addAction(createVirtualSinkAction); - - auto *createVirtualSourceAction = new QAction(QString("Create Virtual Source"), m_view); - connect(createVirtualSourceAction, &QAction::triggered, [this]() { - const int index = ++m_virtualSourceCount; - const QString name = QString("Potato_Virtual_Source_%1").arg(index); - const QString description = QString("Virtual Source %1").arg(index); - if (!m_controller->createVirtualSource(name, description, 2, 48000)) { - qWarning() << "Failed to create virtual source" << name; - } - }); - m_view->addAction(createVirtualSourceAction); - syncGraph(); if (m_model->hasOverlaps()) { m_model->autoArrange(); @@ -349,11 +205,6 @@ GraphEditorWidget::GraphEditorWidget(Potato::PipeWireController *controller, QWi m_meterTimer->setTimerType(Qt::PreciseTimer); connect(m_meterTimer, &QTimer::timeout, this, &GraphEditorWidget::updateMeter); m_meterTimer->start(); - - m_meterProfileTimer.start(); - m_meterProfileReady = true; - - m_presetManager = new PresetManager(m_controller, m_model, this); } static bool isAudioEndpoint(const Potato::NodeInfo &node) @@ -374,7 +225,6 @@ void GraphEditorWidget::syncGraph() if (isAudioEndpoint(node)) { m_model->addPipeWireNode(node); refreshNodeMeter(node.id, node); - refreshMixerStrip(node.id, node); } } const QVector links = m_controller->links(); @@ -405,7 +255,6 @@ void GraphEditorWidget::onNodeAdded(const Potato::NodeInfo &node) if (isAudioEndpoint(node)) { m_model->addPipeWireNode(node); refreshNodeMeter(node.id, node); - refreshMixerStrip(node.id, node); } } @@ -421,8 +270,6 @@ void GraphEditorWidget::onNodeChanged(const Potato::NodeInfo &node) refreshNodeMeter(node.id, node); updateNodeMeterState(node.id, node); - refreshMixerStrip(node.id, node); - updateMixerState(node.id, node); } void GraphEditorWidget::onNodeRemoved(uint32_t nodeId) @@ -441,8 +288,6 @@ void GraphEditorWidget::onNodeRemoved(uint32_t nodeId) m_nodeMeters.remove(nodeId); row->deleteLater(); } - - removeMixerStrip(nodeId); } void GraphEditorWidget::onLinkAdded(const Potato::LinkInfo &link) @@ -538,7 +383,6 @@ void GraphEditorWidget::onConnectionCreated(QtNodes::ConnectionId const connecti const uint32_t linkId = m_controller->createLink(outInfo->id, outputPortId, inInfo->id, inputPortId); if (linkId == 0) { - m_model->deleteConnection(connectionId); return; } @@ -591,12 +435,6 @@ void GraphEditorWidget::updateMeter() return; } - if (!isVisible()) { - return; - } - - const qint64 startNanos = m_meterProfileReady ? m_meterProfileTimer.nsecsElapsed() : 0; - const float peak = m_controller->meterPeak(); m_meter->setLevel(peak); @@ -605,27 +443,6 @@ void GraphEditorWidget::updateMeter() const float nodePeak = m_controller->nodeMeterPeak(nodeId); it.value()->setLevel(nodePeak); } - - for (auto it = m_mixerMeters.begin(); it != m_mixerMeters.end(); ++it) { - const uint32_t nodeId = static_cast(it.key()); - const float nodePeak = m_controller->nodeMeterPeak(nodeId); - it.value()->setLevel(nodePeak); - } - - if (m_meterProfileReady) { - const qint64 duration = m_meterProfileTimer.nsecsElapsed() - startNanos; - m_meterProfileNanos += duration; - m_meterProfileMax = std::max(m_meterProfileMax, duration); - ++m_meterProfileFrames; - if (m_meterProfileFrames >= 300) { - const double avgMs = static_cast(m_meterProfileNanos) / (1000000.0 * m_meterProfileFrames); - const double maxMs = static_cast(m_meterProfileMax) / 1000000.0; - qInfo() << "Meter update avg" << avgMs << "ms max" << maxMs << "ms"; - m_meterProfileNanos = 0; - m_meterProfileMax = 0; - m_meterProfileFrames = 0; - } - } } void GraphEditorWidget::updateLayoutState() @@ -641,28 +458,6 @@ void GraphEditorWidget::updateLayoutState() } } -void GraphEditorWidget::reloadGraphFromController() -{ - m_ignoreCreate.clear(); - m_ignoreDelete.clear(); - m_connectionToLinkId.clear(); - m_linkIdToConnection.clear(); - m_nodeLinkCounts.clear(); - m_linksById.clear(); - - m_model->reset(); - syncGraph(); - - double viewScale = 1.0; - QPointF viewCenter; - if (m_model->viewState(viewScale, viewCenter)) { - m_view->setupScale(viewScale); - m_view->centerOn(viewCenter); - } else { - m_view->zoomFitAll(); - } -} - void GraphEditorWidget::refreshNodeMeter(uint32_t nodeId, const Potato::NodeInfo &node) { if (m_nodeMeterRows.contains(nodeId)) { @@ -821,242 +616,3 @@ bool GraphEditorWidget::eventFilter(QObject *object, QEvent *event) return QWidget::eventFilter(object, event); } - -void GraphEditorWidget::refreshMixerStrip(uint32_t nodeId, const Potato::NodeInfo &node) -{ - if (m_mixerStrips.contains(nodeId)) { - return; - } - - auto *strip = new QWidget(m_mixerList); - strip->setFixedWidth(90); - auto *layout = new QVBoxLayout(strip); - layout->setContentsMargins(0, 0, 0, 0); - layout->setSpacing(8); - - auto *meter = new AudioLevelMeter(strip); - meter->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); - - auto *fader = new ClickSlider(Qt::Vertical, strip); - fader->setRange(0, 100); - fader->setValue(100); - fader->setToolTip("Volume"); - fader->setStyleSheet( - "QSlider::groove:vertical { width: 4px; background: #2b2f38; border-radius: 2px; }" - "QSlider::sub-page:vertical { background: #2b2f38; border-radius: 2px; }" - "QSlider::add-page:vertical { background: #5fcf8d; border-radius: 2px; }" - "QSlider::handle:vertical { height: 10px; margin: 0 -6px; background: #dbe2ee; border-radius: 5px; }" - ); - - auto *muteBtn = new QToolButton(strip); - muteBtn->setText("M"); - muteBtn->setCheckable(true); - muteBtn->setFixedSize(24, 24); - muteBtn->setToolTip("Mute"); - muteBtn->setStyleSheet( - "QToolButton { background: #2b2f38; color: #8c94a5; border: none; border-radius: 4px; font-weight: 700; }" - "QToolButton:checked { background: #c94f4f; color: #ffffff; }" - "QToolButton:hover { background: #3c4350; }" - ); - - auto *soloBtn = new QToolButton(strip); - soloBtn->setText("S"); - soloBtn->setCheckable(true); - soloBtn->setFixedSize(24, 24); - soloBtn->setToolTip("Solo"); - soloBtn->setStyleSheet( - "QToolButton { background: #2b2f38; color: #8c94a5; border: none; border-radius: 4px; font-weight: 700; }" - "QToolButton:checked { background: #e0b045; color: #ffffff; }" - "QToolButton:hover { background: #3c4350; }" - ); - - auto *label = new QLabel(node.description.isEmpty() ? node.name : node.description, strip); - label->setStyleSheet("color: #8c94a5; font-size: 10px; font-weight: 600;"); - label->setAlignment(Qt::AlignCenter); - label->setWordWrap(true); - label->setMinimumHeight(48); - - layout->addWidget(meter, 1); - layout->addWidget(fader, 0, Qt::AlignHCenter); - - auto *btnLayout = new QHBoxLayout(); - btnLayout->setContentsMargins(0, 0, 0, 0); - btnLayout->setSpacing(4); - btnLayout->addWidget(muteBtn); - btnLayout->addWidget(soloBtn); - layout->addLayout(btnLayout); - - layout->addWidget(label); - - m_mixerListLayout->addWidget(strip); - - m_mixerStrips.insert(nodeId, strip); - m_mixerMeters.insert(nodeId, meter); - m_mixerFaders.insert(nodeId, fader); - m_mixerMutes.insert(nodeId, muteBtn); - m_mixerSolos.insert(nodeId, soloBtn); - m_mixerUserMute.insert(nodeId, false); - m_mixerLastState.insert(nodeId, NodeVolumeState{1.0f, false}); - - connect(fader, &QSlider::valueChanged, [this, nodeId](int value) { - if (!m_mixerMutes.contains(nodeId) || !m_mixerFaders.contains(nodeId)) { - return; - } - const float volume = static_cast(value) / 100.0f; - const bool userMute = m_mixerUserMute.value(nodeId, false); - if (m_mixerFaders[nodeId]->isSliderDown()) { - if (!m_mixerStartState.contains(nodeId)) { - const NodeVolumeState previous = m_mixerLastState.value(nodeId, NodeVolumeState{volume, userMute}); - m_mixerStartState.insert(nodeId, previous); - } - } else { - const NodeVolumeState previous = m_mixerLastState.value(nodeId, NodeVolumeState{volume, userMute}); - const NodeVolumeState next{volume, userMute}; - pushVolumeCommand(nodeId, previous, next); - m_mixerLastState.insert(nodeId, next); - } - applySoloState(); - }); - - connect(fader, &QSlider::sliderPressed, [this, nodeId]() { - if (!m_mixerFaders.contains(nodeId)) { - return; - } - const float volume = static_cast(m_mixerFaders[nodeId]->value()) / 100.0f; - const bool userMute = m_mixerUserMute.value(nodeId, false); - const NodeVolumeState previous = m_mixerLastState.value(nodeId, NodeVolumeState{volume, userMute}); - m_mixerStartState.insert(nodeId, previous); - }); - - connect(fader, &QSlider::sliderReleased, [this, nodeId]() { - if (!m_mixerFaders.contains(nodeId)) { - return; - } - const float volume = static_cast(m_mixerFaders[nodeId]->value()) / 100.0f; - const bool userMute = m_mixerUserMute.value(nodeId, false); - const NodeVolumeState previous = m_mixerStartState.value(nodeId, m_mixerLastState.value(nodeId, NodeVolumeState{volume, userMute})); - const NodeVolumeState next{volume, userMute}; - m_mixerStartState.remove(nodeId); - pushVolumeCommand(nodeId, previous, next); - m_mixerLastState.insert(nodeId, next); - }); - - connect(muteBtn, &QToolButton::toggled, [this, nodeId](bool checked) { - if (!m_mixerFaders.contains(nodeId)) { - return; - } - const float volume = static_cast(m_mixerFaders[nodeId]->value()) / 100.0f; - const bool previousMute = m_mixerUserMute.value(nodeId, false); - const NodeVolumeState previous{volume, previousMute}; - const NodeVolumeState next{volume, checked}; - m_mixerUserMute[nodeId] = checked; - applySoloState(); - pushVolumeCommand(nodeId, previous, next); - m_mixerLastState.insert(nodeId, next); - }); - - connect(soloBtn, &QToolButton::toggled, [this, nodeId](bool checked) { - if (checked) { - m_mixerSoloNodes.insert(nodeId); - } else { - m_mixerSoloNodes.remove(nodeId); - } - applySoloState(); - }); - - updateMixerState(nodeId, node); -} - -void GraphEditorWidget::updateMixerState(uint32_t nodeId, const Potato::NodeInfo &node) -{ - if (!m_mixerFaders.contains(nodeId) || !m_mixerMutes.contains(nodeId)) { - return; - } - if (node.stableId.isEmpty() || !m_model) { - return; - } - - NodeVolumeState state; - if (!m_model->nodeVolumeState(nodeId, state)) { - return; - } - - auto *fader = m_mixerFaders.value(nodeId); - auto *muteBtn = m_mixerMutes.value(nodeId); - fader->blockSignals(true); - muteBtn->blockSignals(true); - fader->setValue(static_cast(state.volume * 100.0f)); - muteBtn->setChecked(state.mute); - fader->blockSignals(false); - muteBtn->blockSignals(false); - m_mixerUserMute[nodeId] = state.mute; - m_mixerLastState.insert(nodeId, state); - applySoloState(); -} - -void GraphEditorWidget::removeMixerStrip(uint32_t nodeId) -{ - if (m_mixerStrips.contains(nodeId)) { - auto *strip = m_mixerStrips.take(nodeId); - m_mixerMeters.remove(nodeId); - m_mixerFaders.remove(nodeId); - m_mixerMutes.remove(nodeId); - m_mixerSolos.remove(nodeId); - m_mixerUserMute.remove(nodeId); - m_mixerStartState.remove(nodeId); - m_mixerLastState.remove(nodeId); - m_mixerSoloNodes.remove(nodeId); - strip->deleteLater(); - } -} - -void GraphEditorWidget::applySoloState() -{ - for (auto it = m_mixerFaders.begin(); it != m_mixerFaders.end(); ++it) { - const uint32_t nodeId = static_cast(it.key()); - const float volume = static_cast(it.value()->value()) / 100.0f; - const bool userMute = m_mixerUserMute.value(nodeId, false); - const bool soloActive = !m_mixerSoloNodes.isEmpty(); - const bool effectiveMute = userMute || (soloActive && !m_mixerSoloNodes.contains(nodeId)); - m_controller->setNodeVolume(nodeId, volume, effectiveMute); - if (m_model) { - m_model->setNodeVolumeState(nodeId, NodeVolumeState{volume, userMute}, false); - } - } -} - -void GraphEditorWidget::applyVolumeState(uint32_t nodeId, const NodeVolumeState &state, bool updateMixer) -{ - Q_UNUSED(updateMixer) - m_ignoreVolumeUndo = true; - if (m_model) { - m_model->setNodeVolumeState(nodeId, state, false); - } - m_mixerUserMute[nodeId] = state.mute; - if (m_mixerFaders.contains(nodeId)) { - m_mixerFaders[nodeId]->blockSignals(true); - m_mixerFaders[nodeId]->setValue(static_cast(state.volume * 100.0f)); - m_mixerFaders[nodeId]->blockSignals(false); - } - if (m_mixerMutes.contains(nodeId)) { - m_mixerMutes[nodeId]->blockSignals(true); - m_mixerMutes[nodeId]->setChecked(state.mute); - m_mixerMutes[nodeId]->blockSignals(false); - } - m_mixerLastState.insert(nodeId, state); - m_mixerStartState.remove(nodeId); - applySoloState(); - m_ignoreVolumeUndo = false; -} - -void GraphEditorWidget::pushVolumeCommand(uint32_t nodeId, const NodeVolumeState &previous, const NodeVolumeState &next) -{ - if (!m_scene || m_ignoreVolumeUndo) { - return; - } - const bool changedVolume = qAbs(previous.volume - next.volume) > 0.0001f; - if (!changedVolume && previous.mute == next.mute) { - return; - } - m_scene->undoStack().push(new VolumeChangeCommand(this, nodeId, previous, next)); -} diff --git a/src/gui/GraphEditorWidget.h b/src/gui/GraphEditorWidget.h index 31977f4..0a7ecb3 100644 --- a/src/gui/GraphEditorWidget.h +++ b/src/gui/GraphEditorWidget.h @@ -9,7 +9,6 @@ #include #include #include -#include #include class AudioLevelMeter; @@ -17,20 +16,12 @@ class QLabel; class QTimer; class QScrollArea; class QVBoxLayout; -class QHBoxLayout; -class QSlider; -class QToolButton; class QSplitter; -class QTabWidget; -class PresetManager; -class VolumeChangeCommand; class GraphEditorWidget : public QWidget { Q_OBJECT - friend class VolumeChangeCommand; - public: explicit GraphEditorWidget(Potato::PipeWireController *controller, QWidget *parent = nullptr); @@ -51,16 +42,9 @@ private: void updateLayoutState(); void updateNodeMeterLabel(QLabel *label); void updateNodeMeterState(uint32_t nodeId, const Potato::NodeInfo &node); - void refreshMixerStrip(uint32_t nodeId, const Potato::NodeInfo &node); - void updateMixerState(uint32_t nodeId, const Potato::NodeInfo &node); - void removeMixerStrip(uint32_t nodeId); - void applySoloState(); - void applyVolumeState(uint32_t nodeId, const NodeVolumeState &state, bool updateMixer); - void pushVolumeCommand(uint32_t nodeId, const NodeVolumeState &previous, const NodeVolumeState &next); void handleLinkRemoved(uint32_t linkId); bool isMeterNode(uint32_t nodeId) const; int activeLinkCount(uint32_t nodeId) const; - void reloadGraphFromController(); QString connectionKey(const QtNodes::ConnectionId &connectionId) const; bool eventFilter(QObject *object, QEvent *event) override; @@ -70,27 +54,11 @@ private: QtNodes::GraphicsView *m_view = nullptr; QSplitter *m_splitter = nullptr; - QTabWidget *m_sidebarTabs = nullptr; - QWidget *m_mixerTab = nullptr; - QWidget *m_mixerList = nullptr; - QHBoxLayout *m_mixerListLayout = nullptr; - QMap m_mixerMeters; - QMap m_mixerStrips; - QMap m_mixerFaders; - QMap m_mixerMutes; - QMap m_mixerSolos; - QMap m_mixerUserMute; - QMap m_mixerStartState; - QMap m_mixerLastState; - QSet m_mixerSoloNodes; - QSet m_ignoreCreate; QSet m_ignoreDelete; QSet m_ignoreLinkRemoved; QMap m_connectionToLinkId; QMap m_linkIdToConnection; - int m_virtualSinkCount = 0; - int m_virtualSourceCount = 0; AudioLevelMeter *m_meter = nullptr; QTimer *m_meterTimer = nullptr; QScrollArea *m_meterScroll = nullptr; @@ -101,11 +69,4 @@ private: QMap m_nodeMeterLabels; QMap m_nodeLinkCounts; QMap m_linksById; - QElapsedTimer m_meterProfileTimer; - bool m_meterProfileReady = false; - qint64 m_meterProfileNanos = 0; - qint64 m_meterProfileMax = 0; - int m_meterProfileFrames = 0; - PresetManager *m_presetManager = nullptr; - bool m_ignoreVolumeUndo = false; }; diff --git a/src/gui/PipeWireGraphModel.cpp b/src/gui/PipeWireGraphModel.cpp index de76004..06c1455 100644 --- a/src/gui/PipeWireGraphModel.cpp +++ b/src/gui/PipeWireGraphModel.cpp @@ -16,18 +16,11 @@ #include #include #include -#include -#include -#include "gui/ClickSlider.h" -#include -#include -#include #include #include #include -#include #include #include @@ -115,103 +108,6 @@ PipeWireGraphModel::PipeWireGraphModel(Potato::PipeWireController *controller, Q } } -QWidget *PipeWireGraphModel::nodeWidget(QtNodes::NodeId nodeId) const -{ - auto it = m_nodeWidgets.find(nodeId); - if (it != m_nodeWidgets.end()) { - return it->second; - } - - uint32_t pipewireId = 0; - auto nodeIt = m_nodes.find(nodeId); - if (nodeIt != m_nodes.end()) { - pipewireId = nodeIt->second.id; - } - - auto *widget = new QWidget(); - auto *layout = new QHBoxLayout(widget); - layout->setContentsMargins(6, 2, 6, 2); - layout->setSpacing(6); - - auto *muteButton = new QToolButton(widget); - muteButton->setText("M"); - muteButton->setCheckable(true); - muteButton->setFixedSize(20, 20); - muteButton->setToolTip(QString("Mute")); - - auto *slider = new ClickSlider(Qt::Horizontal, widget); - slider->setRange(0, 100); - slider->setValue(100); - slider->setFixedHeight(18); - slider->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); - slider->setToolTip(QString("Volume")); - - if (pipewireId != 0 && m_nodeVolumeState.contains(pipewireId)) { - const NodeVolumeState state = m_nodeVolumeState.value(pipewireId); - slider->setValue(static_cast(state.volume * 100.0f)); - muteButton->setChecked(state.mute); - } - - const auto applyVolume = [this, pipewireId, slider, muteButton]() { - if (!m_controller || pipewireId == 0) { - return; - } - const float volume = static_cast(slider->value()) / 100.0f; - const NodeVolumeState next{volume, muteButton->isChecked()}; - const NodeVolumeState previous = m_nodeVolumeState.value(pipewireId, next); - m_controller->setNodeVolume(pipewireId, volume, next.mute); - auto *self = const_cast(this); - self->setNodeVolumeState(pipewireId, next, false); - if (!slider->isSliderDown() && !m_inlineStartState.contains(pipewireId)) { - self->emitNodeVolumeChanged(pipewireId, previous, next); - } - }; - - QObject::connect(slider, &QSlider::valueChanged, widget, [applyVolume](int) { applyVolume(); }); - QObject::connect(muteButton, &QToolButton::toggled, widget, [applyVolume](bool) { applyVolume(); }); - QObject::connect(slider, &QSlider::sliderPressed, widget, [this, pipewireId, slider, muteButton]() { - if (pipewireId == 0) { - return; - } - bool ok = false; - const int pressValue = slider->property("pressValue").toInt(&ok); - const float volume = ok ? (static_cast(pressValue) / 100.0f) - : static_cast(slider->value()) / 100.0f; - const bool mute = muteButton->isChecked(); - m_inlineStartState.insert(pipewireId, NodeVolumeState{volume, mute}); - }); - QObject::connect(slider, &QSlider::sliderReleased, widget, [this, pipewireId, slider, muteButton]() { - if (pipewireId == 0) { - return; - } - const NodeVolumeState previous = m_inlineStartState.value(pipewireId, m_nodeVolumeState.value(pipewireId)); - m_inlineStartState.remove(pipewireId); - const float volume = static_cast(slider->value()) / 100.0f; - const NodeVolumeState next{volume, muteButton->isChecked()}; - auto *self = const_cast(this); - self->emitNodeVolumeChanged(pipewireId, previous, next); - }); - - layout->addWidget(muteButton); - layout->addWidget(slider); - - widget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); - widget->setFixedHeight(26); - widget->setStyleSheet( - "QToolButton { background: #2b2f38; color: #dbe2ee; border: 1px solid #39404c; border-radius: 4px; font-weight: 700; }" - "QToolButton:checked { background: #3c4350; color: #ffcf7a; }" - "QSlider::groove:horizontal { height: 4px; background: #2b2f38; border-radius: 2px; }" - "QSlider::sub-page:horizontal { background: #5fcf8d; border-radius: 2px; }" - "QSlider::add-page:horizontal { background: #2b2f38; border-radius: 2px; }" - "QSlider::handle:horizontal { width: 10px; margin: -6px 0; background: #dbe2ee; border-radius: 5px; }" - ); - - widget->adjustSize(); - - m_nodeWidgets.emplace(nodeId, widget); - return widget; -} - QtNodes::NodeId PipeWireGraphModel::newNodeId() { return m_nextNodeId++; @@ -314,23 +210,6 @@ bool PipeWireGraphModel::connectionPossible(QtNodes::ConnectionId const connecti return false; } - const auto &outPort = outInfo.outputPorts.at(connectionId.outPortIndex); - const auto &inPort = inInfo.inputPorts.at(connectionId.inPortIndex); - if (outPort.direction != SPA_DIRECTION_OUTPUT || inPort.direction != SPA_DIRECTION_INPUT) { - return false; - } - - const auto isAudioClass = [](Potato::MediaClass mediaClass) { - return mediaClass == Potato::MediaClass::AudioSink - || mediaClass == Potato::MediaClass::AudioSource - || mediaClass == Potato::MediaClass::AudioDuplex - || mediaClass == Potato::MediaClass::Stream; - }; - - if (!isAudioClass(outInfo.mediaClass) || !isAudioClass(inInfo.mediaClass)) { - return false; - } - return true; } @@ -378,10 +257,6 @@ QVariant PipeWireGraphModel::nodeData(QtNodes::NodeId nodeId, QtNodes::NodeRole } case QtNodes::NodeRole::Size: { - auto sizeIt = m_nodeSizes.find(nodeId); - if (sizeIt != m_nodeSizes.end()) { - return sizeIt->second; - } const int maxPorts = std::max(info.inputPorts.size(), info.outputPorts.size()); const int baseHeight = 50; const int perPortHeight = 28; @@ -399,8 +274,6 @@ QVariant PipeWireGraphModel::nodeData(QtNodes::NodeId nodeId, QtNodes::NodeRole return QString("PipeWire"); case QtNodes::NodeRole::Style: return nodeStyleVariant(info); - case QtNodes::NodeRole::Widget: - return QVariant::fromValue(nodeWidget(nodeId)); default: return QVariant(); } @@ -420,11 +293,6 @@ bool PipeWireGraphModel::setNodeData(QtNodes::NodeId nodeId, QtNodes::NodeRole r return true; } - if (role == QtNodes::NodeRole::Size) { - m_nodeSizes[nodeId] = value.toSize(); - return true; - } - return false; } @@ -465,6 +333,9 @@ QVariant PipeWireGraphModel::portData(QtNodes::NodeId nodeId, } if (role == QtNodes::PortRole::ConnectionPolicyRole) { + if (portType == QtNodes::PortType::In) { + return QVariant::fromValue(QtNodes::ConnectionPolicy::One); + } return QVariant::fromValue(QtNodes::ConnectionPolicy::Many); } @@ -508,8 +379,6 @@ bool PipeWireGraphModel::deleteNode(QtNodes::NodeId const nodeId) m_nodes.erase(nodeId); m_positions.erase(nodeId); - m_nodeSizes.erase(nodeId); - m_nodeWidgets.erase(nodeId); Q_EMIT nodeDeleted(nodeId); return true; } @@ -607,7 +476,6 @@ bool PipeWireGraphModel::updatePipeWireNode(const Potato::NodeInfo &node) const QtNodes::NodeId nodeId = it->second; m_nodes[nodeId] = node; - m_nodeSizes.erase(nodeId); Q_EMIT nodeUpdated(nodeId); return true; } @@ -635,8 +503,6 @@ void PipeWireGraphModel::reset() m_nodes.clear(); m_pwToNode.clear(); m_positions.clear(); - m_nodeSizes.clear(); - m_nodeWidgets.clear(); m_nextNodeId = 1; Q_EMIT modelReset(); } @@ -731,168 +597,6 @@ void PipeWireGraphModel::loadLayout() } } -QJsonObject PipeWireGraphModel::layoutJson() const -{ - QJsonObject root; - QJsonArray nodes; - for (auto it = m_layoutByStableId.cbegin(); it != m_layoutByStableId.cend(); ++it) { - QJsonObject item; - item["id"] = it.key(); - item["x"] = it.value().x(); - item["y"] = it.value().y(); - nodes.append(item); - } - root["nodes"] = nodes; - - QJsonObject view; - view["scale"] = m_viewScale; - view["center_x"] = m_viewCenter.x(); - view["center_y"] = m_viewCenter.y(); - root["view"] = view; - - if (m_hasSplitterSizes && !m_splitterSizes.isEmpty()) { - QJsonArray splitter; - for (const auto size : m_splitterSizes) { - splitter.append(size); - } - root["splitter"] = splitter; - } - - return root; -} - -void PipeWireGraphModel::applyLayoutJson(const QJsonObject &root) -{ - m_layoutByStableId.clear(); - m_hasViewState = false; - m_hasSplitterSizes = false; - m_splitterSizes.clear(); - - const QJsonArray nodes = root.value("nodes").toArray(); - applyLayoutData(nodes); - - const QJsonObject view = root.value("view").toObject(); - if (!view.isEmpty()) { - m_viewScale = view.value("scale").toDouble(1.0); - const double x = view.value("center_x").toDouble(0.0); - const double y = view.value("center_y").toDouble(0.0); - m_viewCenter = QPointF(x, y); - m_hasViewState = true; - } - - const QJsonArray splitter = root.value("splitter").toArray(); - if (!splitter.isEmpty()) { - QList sizes; - sizes.reserve(splitter.size()); - for (const auto &value : splitter) { - sizes.append(value.toInt()); - } - if (!sizes.isEmpty()) { - m_splitterSizes = sizes; - m_hasSplitterSizes = true; - } - } - - for (const auto &entry : m_nodes) { - const QtNodes::NodeId nodeId = entry.first; - const Potato::NodeInfo &info = entry.second; - if (!info.stableId.isEmpty() && m_layoutByStableId.contains(info.stableId)) { - const QPointF position = m_layoutByStableId.value(info.stableId); - m_positions[nodeId] = position; - Q_EMIT nodePositionUpdated(nodeId); - } - } -} - -QHash PipeWireGraphModel::volumeStates() const -{ - QHash result; - for (const auto &entry : m_nodes) { - const Potato::NodeInfo &info = entry.second; - if (info.stableId.isEmpty()) { - continue; - } - if (!m_nodeVolumeState.contains(info.id)) { - continue; - } - result.insert(info.stableId, m_nodeVolumeState.value(info.id)); - } - return result; -} - -void PipeWireGraphModel::applyVolumeStates(const QHash &states) -{ - if (!m_controller) { - return; - } - - const QVector nodes = m_controller->nodes(); - for (const auto &node : nodes) { - if (node.stableId.isEmpty() || !states.contains(node.stableId)) { - continue; - } - const NodeVolumeState state = states.value(node.stableId); - setNodeVolumeState(node.id, state, false); - m_controller->setNodeVolume(node.id, state.volume, state.mute); - } -} - -void PipeWireGraphModel::emitNodeVolumeChanged(uint32_t nodeId, const NodeVolumeState &previous, const NodeVolumeState ¤t) -{ - const bool changedVolume = qAbs(previous.volume - current.volume) > 0.0001f; - if (!changedVolume && previous.mute == current.mute) { - return; - } - Q_EMIT nodeVolumeChanged(nodeId, previous, current); -} - -void PipeWireGraphModel::setNodeVolumeState(uint32_t nodeId, const NodeVolumeState &state, bool notify) -{ - const NodeVolumeState previous = m_nodeVolumeState.value(nodeId, NodeVolumeState{}); - m_nodeVolumeState.insert(nodeId, state); - - auto nodeIt = m_pwToNode.find(nodeId); - if (nodeIt == m_pwToNode.end()) { - return; - } - auto widgetIt = m_nodeWidgets.find(nodeIt->second); - if (widgetIt == m_nodeWidgets.end()) { - return; - } - - QWidget *widget = widgetIt->second; - if (!widget) { - return; - } - - if (auto *slider = widget->findChild()) { - slider->blockSignals(true); - slider->setValue(static_cast(state.volume * 100.0f)); - slider->blockSignals(false); - } - if (auto *button = widget->findChild()) { - button->blockSignals(true); - button->setChecked(state.mute); - button->blockSignals(false); - } - - if (notify) { - const bool changedVolume = std::abs(previous.volume - state.volume) > 0.0001f; - if (changedVolume || previous.mute != state.mute) { - Q_EMIT nodeVolumeChanged(nodeId, previous, state); - } - } -} - -bool PipeWireGraphModel::nodeVolumeState(uint32_t nodeId, NodeVolumeState &state) const -{ - if (!m_nodeVolumeState.contains(nodeId)) { - return false; - } - state = m_nodeVolumeState.value(nodeId); - return true; -} - void PipeWireGraphModel::saveLayout() const { const QString path = layoutFilePath(); diff --git a/src/gui/PipeWireGraphModel.h b/src/gui/PipeWireGraphModel.h index b55e7b7..891df31 100644 --- a/src/gui/PipeWireGraphModel.h +++ b/src/gui/PipeWireGraphModel.h @@ -16,13 +16,6 @@ #include #include -class QWidget; - -struct NodeVolumeState { - float volume = 1.0f; - bool mute = false; -}; - class PipeWireGraphModel : public QtNodes::AbstractGraphModel { Q_OBJECT @@ -77,19 +70,8 @@ public: bool viewState(double &scale, QPointF ¢er) const; void setSplitterSizes(const QList &sizes); bool splitterSizes(QList &sizes) const; - QJsonObject layoutJson() const; - void applyLayoutJson(const QJsonObject &root); - QHash volumeStates() const; - void applyVolumeStates(const QHash &states); - void setNodeVolumeState(uint32_t nodeId, const NodeVolumeState &state, bool notify = true); - bool nodeVolumeState(uint32_t nodeId, NodeVolumeState &state) const; - -signals: - void nodeVolumeChanged(uint32_t nodeId, const NodeVolumeState &previous, const NodeVolumeState ¤t); private: - QWidget *nodeWidget(QtNodes::NodeId nodeId) const; - void emitNodeVolumeChanged(uint32_t nodeId, const NodeVolumeState &previous, const NodeVolumeState ¤t); QtNodes::ConnectionId connectionFromPipeWire(const Potato::LinkInfo &link, bool *ok) const; bool findPortIndex(const Potato::NodeInfo &node, uint32_t portId, QtNodes::PortType type, QtNodes::PortIndex &index) const; QString portLabel(const Potato::PortInfo &port) const; @@ -113,8 +95,4 @@ private: bool m_hasViewState = false; QList m_splitterSizes; bool m_hasSplitterSizes = false; - mutable std::unordered_map m_nodeWidgets; - std::unordered_map m_nodeSizes; - mutable QHash m_nodeVolumeState; - mutable QHash m_inlineStartState; }; diff --git a/src/pipewire/pipewirecontroller.cpp b/src/pipewire/pipewirecontroller.cpp index c85d56c..58c2079 100644 --- a/src/pipewire/pipewirecontroller.cpp +++ b/src/pipewire/pipewirecontroller.cpp @@ -5,7 +5,6 @@ #include #include #include -#include #include #include #include @@ -14,119 +13,14 @@ #include #include #include -#include #include #include #include #include -#include #include namespace Potato { -static constexpr uint32_t kMeterRingCapacityBytes = 4096; - -static void writeRingValue(spa_ringbuffer *ring, std::vector &buffer, float value) -{ - if (!ring || buffer.empty()) { - return; - } - - uint32_t index = 0; - const uint32_t avail = spa_ringbuffer_get_write_index(ring, &index); - if (avail < sizeof(float)) { - return; - } - - const uint32_t size = static_cast(buffer.size()); - uint32_t offset = index & (size - 1); - if (offset + sizeof(float) <= size) { - std::memcpy(buffer.data() + offset, &value, sizeof(float)); - } else { - const uint32_t first = size - offset; - std::memcpy(buffer.data() + offset, &value, first); - std::memcpy(buffer.data(), reinterpret_cast(&value) + first, sizeof(float) - first); - } - spa_ringbuffer_write_update(ring, index + sizeof(float)); -} - -static bool readRingLatest(spa_ringbuffer *ring, std::vector &buffer, float &value) -{ - if (!ring || buffer.empty()) { - return false; - } - - uint32_t index = 0; - const uint32_t avail = spa_ringbuffer_get_read_index(ring, &index); - if (avail < sizeof(float)) { - return false; - } - - const uint32_t size = static_cast(buffer.size()); - const uint32_t latestIndex = index + (avail - sizeof(float)); - uint32_t offset = latestIndex & (size - 1); - if (offset + sizeof(float) <= size) { - std::memcpy(&value, buffer.data() + offset, sizeof(float)); - } else { - const uint32_t first = size - offset; - std::memcpy(&value, buffer.data() + offset, first); - std::memcpy(reinterpret_cast(&value) + first, buffer.data(), sizeof(float) - first); - } - - spa_ringbuffer_read_update(ring, index + avail); - return true; -} - -bool PipeWireController::createVirtualDevice(const QString &name, - const QString &description, - const char *factoryName, - const char *mediaClass, - int channels, - int rate) -{ - if (!m_threadLoop || !m_core || name.isEmpty()) { - return false; - } - - channels = channels > 0 ? channels : 2; - rate = rate > 0 ? rate : 48000; - - const QByteArray nameBytes = name.toUtf8(); - const QByteArray descBytes = description.isEmpty() ? nameBytes : description.toUtf8(); - const QByteArray channelsBytes = QByteArray::number(channels); - const QByteArray rateBytes = QByteArray::number(rate); - - struct spa_dict_item items[] = { - { PW_KEY_FACTORY_NAME, factoryName }, - { PW_KEY_NODE_NAME, nameBytes.constData() }, - { PW_KEY_NODE_DESCRIPTION, descBytes.constData() }, - { PW_KEY_MEDIA_CLASS, mediaClass }, - { PW_KEY_AUDIO_CHANNELS, channelsBytes.constData() }, - { PW_KEY_AUDIO_RATE, rateBytes.constData() }, - { "object.linger", "true" }, - { PW_KEY_APP_NAME, "Potato-Manager" } - }; - - struct spa_dict dict = SPA_DICT_INIT(items, SPA_N_ELEMENTS(items)); - - lock(); - auto *proxy = static_cast(pw_core_create_object( - m_core, - "adapter", - PW_TYPE_INTERFACE_Node, - PW_VERSION_NODE, - &dict, - 0)); - unlock(); - - if (!proxy) { - return false; - } - - m_virtualDevices.push_back(proxy); - return true; -} - static QString toQString(const char *value) { if (!value) { @@ -156,7 +50,7 @@ void registryEventGlobal(void *data, uint32_t id, uint32_t permissions, void registryEventGlobalRemove(void *data, uint32_t id) { auto *self = static_cast(data); - + { QMutexLocker lock(&self->m_nodesMutex); if (self->m_nodes.contains(id)) { @@ -164,27 +58,18 @@ void registryEventGlobalRemove(void *data, uint32_t id) emit self->nodeRemoved(id); return; } - + if (self->m_ports.contains(id)) { self->m_ports.remove(id); return; } - } - - bool linkRemoved = false; - { - QMutexLocker lock(&self->m_nodesMutex); + if (self->m_links.contains(id)) { self->m_links.remove(id); - linkRemoved = true; + emit self->linkRemoved(id); + return; } } - - if (linkRemoved) { - emit self->linkRemoved(id); - self->handleLinkRemoval(id); - return; - } } void coreEventDone(void *data, uint32_t id, int seq) @@ -265,9 +150,6 @@ void meterProcess(void *data) } self->m_meterPeak.store(peak, std::memory_order_relaxed); - if (self->m_meterRingReady.load(std::memory_order_relaxed)) { - writeRingValue(&self->m_meterRing, self->m_meterRingData, peak); - } pw_stream_queue_buffer(self->m_meterStream, buf); } @@ -335,9 +217,6 @@ PipeWireController::PipeWireController(QObject *parent) { m_registryListener = new spa_hook; m_coreListener = new spa_hook; - m_meterRingData.resize(kMeterRingCapacityBytes); - spa_ringbuffer_init(&m_meterRing); - m_meterRingReady.store(true, std::memory_order_relaxed); } PipeWireController::~PipeWireController() @@ -442,13 +321,6 @@ void PipeWireController::shutdown() } m_nodeMeters.clear(); } - - for (auto *proxy : m_virtualDevices) { - if (proxy) { - pw_proxy_destroy(proxy); - } - } - m_virtualDevices.clear(); if (m_core) { pw_core_disconnect(m_core); @@ -468,9 +340,6 @@ void PipeWireController::shutdown() } pw_deinit(); - - m_meterRingReady.store(false, std::memory_order_relaxed); - m_meterRingData.clear(); m_initialized.storeRelaxed(false); m_connected.storeRelaxed(false); @@ -503,58 +372,7 @@ QVector PipeWireController::links() const float PipeWireController::meterPeak() const { - float peak = m_meterPeak.load(std::memory_order_relaxed); - if (m_meterRingReady.load(std::memory_order_relaxed)) { - float ringPeak = 0.0f; - if (readRingLatest(&m_meterRing, m_meterRingData, ringPeak)) { - peak = ringPeak; - } - } - return peak; -} - -bool PipeWireController::setNodeVolume(uint32_t nodeId, float volume, bool mute) -{ - if (!m_threadLoop || !m_core || !m_registry) { - return false; - } - - if (nodeId == 0) { - return false; - } - - volume = std::clamp(volume, 0.0f, 1.0f); - - lock(); - auto *node = static_cast( - pw_registry_bind(m_registry, nodeId, PW_TYPE_INTERFACE_Node, PW_VERSION_NODE, 0)); - if (!node) { - unlock(); - return false; - } - - uint8_t buffer[128]; - spa_pod_builder builder = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); - auto *param = reinterpret_cast(spa_pod_builder_add_object( - &builder, - SPA_TYPE_OBJECT_Props, SPA_PARAM_Props, - SPA_PROP_volume, SPA_POD_Float(volume), - SPA_PROP_mute, SPA_POD_Bool(mute))); - - pw_node_set_param(node, SPA_PARAM_Props, 0, param); - pw_proxy_destroy(reinterpret_cast(node)); - unlock(); - return true; -} - -bool PipeWireController::createVirtualSink(const QString &name, const QString &description, int channels, int rate) -{ - return createVirtualDevice(name, description, "support.null-audio-sink", "Audio/Sink", channels, rate); -} - -bool PipeWireController::createVirtualSource(const QString &name, const QString &description, int channels, int rate) -{ - return createVirtualDevice(name, description, "support.null-audio-sink", "Audio/Source", channels, rate); + return m_meterPeak.load(std::memory_order_relaxed); } float PipeWireController::nodeMeterPeak(uint32_t nodeId) const @@ -778,11 +596,6 @@ bool PipeWireController::destroyLink(uint32_t linkId) return false; } linkInfo = m_links.value(linkId); - m_userRemovedLinks.insert(linkId); - if (m_linkIntentKeys.contains(linkId)) { - const QString key = m_linkIntentKeys.take(linkId); - m_linkIntents.remove(key); - } } lock(); @@ -796,156 +609,21 @@ bool PipeWireController::destroyLink(uint32_t linkId) unlock(); - qInfo() << "Link destroy requested:" << linkId; - return true; -} - -void PipeWireController::rememberLinkIntent(const LinkInfo &link) -{ - QString key; - if (!buildLinkIntentKey(link, key)) { - return; - } - - QMutexLocker lock(&m_nodesMutex); - m_linkIntents.insert(key); - m_linkIntentKeys.insert(link.id, key); -} - -void PipeWireController::handleLinkRemoval(uint32_t linkId) -{ - QMutexLocker lock(&m_nodesMutex); - if (m_userRemovedLinks.contains(linkId)) { - m_userRemovedLinks.remove(linkId); - } - if (m_linkIntentKeys.contains(linkId)) { - m_linkIntentKeys.remove(linkId); - } -} - -void PipeWireController::tryRestoreLinks() -{ - QList intents; - { - QMutexLocker lock(&m_nodesMutex); - intents = m_linkIntents.values(); - } - - for (const auto &key : intents) { - uint32_t outNodeId = 0; - uint32_t outPortId = 0; - uint32_t inNodeId = 0; - uint32_t inPortId = 0; - if (!resolveLinkIntentKey(key, outNodeId, outPortId, inNodeId, inPortId)) { - continue; - } - createLink(outNodeId, outPortId, inNodeId, inPortId); - } -} - -void PipeWireController::updateLinkIntentsForNode(uint32_t nodeId) -{ - QVector links; - { - QMutexLocker lock(&m_nodesMutex); - for (auto it = m_links.cbegin(); it != m_links.cend(); ++it) { - const LinkInfo &link = it.value(); - if (link.outputNodeId == nodeId || link.inputNodeId == nodeId) { - links.append(link); + QElapsedTimer timer; + timer.start(); + while (timer.elapsed() < 2000) { + { + QMutexLocker lock(&m_nodesMutex); + if (!m_links.contains(linkId)) { + qInfo() << "Link destroyed:" << linkId; + return true; } } + QThread::msleep(10); } - for (const auto &link : links) { - rememberLinkIntent(link); - } -} - -static bool isMeterNodeName(const QString &name) -{ - return name.startsWith("Potato-Meter") || name.startsWith("Potato-Node-Meter"); -} - -bool PipeWireController::buildLinkIntentKey(const LinkInfo &link, QString &key) const -{ - QMutexLocker lock(&m_nodesMutex); - if (!m_nodes.contains(link.outputNodeId) || !m_nodes.contains(link.inputNodeId)) { - return false; - } - - const NodeInfo &outNode = m_nodes.value(link.outputNodeId); - const NodeInfo &inNode = m_nodes.value(link.inputNodeId); - if (isMeterNodeName(outNode.name) || isMeterNodeName(inNode.name)) { - return false; - } - - QString outPortName; - QString inPortName; - for (const auto &port : outNode.outputPorts) { - if (port.id == link.outputPortId) { - outPortName = port.name; - break; - } - } - for (const auto &port : inNode.inputPorts) { - if (port.id == link.inputPortId) { - inPortName = port.name; - break; - } - } - if (outPortName.isEmpty() || inPortName.isEmpty()) { - return false; - } - if (outNode.stableId.isEmpty() || inNode.stableId.isEmpty()) { - return false; - } - - key = QString("%1||%2>>%3||%4").arg(outNode.stableId, outPortName, inNode.stableId, inPortName); - return true; -} - -bool PipeWireController::resolveLinkIntentKey(const QString &key, uint32_t &outNodeId, uint32_t &outPortId, - uint32_t &inNodeId, uint32_t &inPortId) const -{ - const QStringList halves = key.split(">>"); - if (halves.size() != 2) { - return false; - } - const QStringList outParts = halves.at(0).split("||"); - const QStringList inParts = halves.at(1).split("||"); - if (outParts.size() != 2 || inParts.size() != 2) { - return false; - } - - const QString outStableId = outParts.at(0); - const QString outPortName = outParts.at(1); - const QString inStableId = inParts.at(0); - const QString inPortName = inParts.at(1); - - QMutexLocker lock(&m_nodesMutex); - for (const auto &nodeEntry : m_nodes) { - const NodeInfo &node = nodeEntry; - if (node.stableId == outStableId) { - for (const auto &port : node.outputPorts) { - if (port.name == outPortName) { - outNodeId = node.id; - outPortId = port.id; - break; - } - } - } - if (node.stableId == inStableId) { - for (const auto &port : node.inputPorts) { - if (port.name == inPortName) { - inNodeId = node.id; - inPortId = port.id; - break; - } - } - } - } - - return outNodeId != 0 && outPortId != 0 && inNodeId != 0 && inPortId != 0; + qWarning() << "Link destroy requested but ID still present" << linkId; + return false; } QString PipeWireController::dumpGraph() const @@ -1032,9 +710,6 @@ void PipeWireController::handleNodeInfo(uint32_t id, const struct spa_dict *prop qDebug() << "Node changed:" << node.id << node.name; } } - - updateLinkIntentsForNode(id); - tryRestoreLinks(); } void PipeWireController::handlePortInfo(uint32_t id, const struct spa_dict *props) @@ -1062,12 +737,10 @@ void PipeWireController::handlePortInfo(uint32_t id, const struct spa_dict *prop uint32_t nodeId = nodeIdStr ? static_cast(atoi(nodeIdStr)) : 0; PortInfo port(id, nodeId, portName, direction); - bool emitChanged = false; - NodeInfo nodeSnapshot; { QMutexLocker lock(&m_nodesMutex); m_ports.insert(id, port); - + if (nodeId != 0 && m_nodes.contains(nodeId)) { NodeInfo &node = m_nodes[nodeId]; auto &ports = (direction == PW_DIRECTION_INPUT) ? node.inputPorts : node.outputPorts; @@ -1078,18 +751,9 @@ void PipeWireController::handlePortInfo(uint32_t id, const struct spa_dict *prop } } ports.append(port); - nodeSnapshot = node; - emitChanged = true; } } - - if (emitChanged) { - emit nodeChanged(nodeSnapshot); - } - - updateLinkIntentsForNode(nodeId); - tryRestoreLinks(); - + qDebug() << "Port added:" << id << portName << "direction:" << direction; } @@ -1117,8 +781,6 @@ void PipeWireController::handleLinkInfo(uint32_t id, const struct spa_dict *prop } emit linkAdded(link); - - rememberLinkIntent(link); qDebug() << "Link added:" << id << "from" << outputNode << ":" << outputPort << "to" << inputNode << ":" << inputPort; diff --git a/src/pipewire/pipewirecontroller.h b/src/pipewire/pipewirecontroller.h index dd07e2f..322f95e 100644 --- a/src/pipewire/pipewirecontroller.h +++ b/src/pipewire/pipewirecontroller.h @@ -5,19 +5,13 @@ #include #include #include -#include -#include #include -#include - -#include struct pw_thread_loop; struct pw_context; struct pw_core; struct pw_registry; struct pw_stream; -struct pw_proxy; struct spa_hook; struct spa_dict; namespace Potato { @@ -44,9 +38,6 @@ public: float nodeMeterPeak(uint32_t nodeId) const; void ensureNodeMeter(uint32_t nodeId, const QString &targetName, bool captureSink); void removeNodeMeter(uint32_t nodeId); - bool setNodeVolume(uint32_t nodeId, float volume, bool mute); - bool createVirtualSink(const QString &name, const QString &description, int channels, int rate); - bool createVirtualSource(const QString &name, const QString &description, int channels, int rate); uint32_t createLink(uint32_t outputNodeId, uint32_t outputPortId, uint32_t inputNodeId, uint32_t inputPortId); @@ -81,16 +72,6 @@ private: void handleLinkInfo(uint32_t id, const struct ::spa_dict *props); bool setupMeterStream(); void teardownMeterStream(); - bool createVirtualDevice(const QString &name, const QString &description, - const char *factoryName, const char *mediaClass, - int channels, int rate); - void rememberLinkIntent(const LinkInfo &link); - void handleLinkRemoval(uint32_t linkId); - void tryRestoreLinks(); - void updateLinkIntentsForNode(uint32_t nodeId); - bool buildLinkIntentKey(const LinkInfo &link, QString &key) const; - bool resolveLinkIntentKey(const QString &key, uint32_t &outNodeId, uint32_t &outPortId, - uint32_t &inNodeId, uint32_t &inPortId) const; void lock(); void unlock(); @@ -108,18 +89,10 @@ private: QMap m_nodes; QMap m_ports; QMap m_links; - QSet m_linkIntents; - QHash m_linkIntentKeys; - QSet m_userRemovedLinks; QAtomicInteger m_connected{false}; QAtomicInteger m_initialized{false}; std::atomic m_meterPeak{0.0f}; - mutable spa_ringbuffer m_meterRing{}; - mutable std::vector m_meterRingData; - std::atomic m_meterRingReady{false}; - - std::vector m_virtualDevices; mutable QMutex m_meterMutex; QMap m_nodeMeters; diff --git a/src/presets/PresetManager.cpp b/src/presets/PresetManager.cpp deleted file mode 100644 index fd54274..0000000 --- a/src/presets/PresetManager.cpp +++ /dev/null @@ -1,293 +0,0 @@ -#include "presets/PresetManager.h" - -#include "pipewire/pipewirecontroller.h" -#include "gui/PipeWireGraphModel.h" - -#include -#include -#include -#include - -#include - -PresetManager::PresetManager(Potato::PipeWireController *controller, - PipeWireGraphModel *model, - QObject *parent) - : QObject(parent) - , m_controller(controller) - , m_model(model) -{ -} - -bool PresetManager::savePreset(const QString &path) const -{ - if (!m_controller || !m_model || path.isEmpty()) { - return false; - } - - const QJsonObject root = buildPreset(); - const QJsonDocument doc(root); - - QFile file(path); - if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { - return false; - } - - file.write(doc.toJson(QJsonDocument::Indented)); - return true; -} - -bool PresetManager::loadPreset(const QString &path) -{ - if (!m_controller || !m_model || path.isEmpty()) { - return false; - } - - QFile file(path); - if (!file.open(QIODevice::ReadOnly)) { - return false; - } - - const QJsonDocument doc = QJsonDocument::fromJson(file.readAll()); - if (!doc.isObject()) { - return false; - } - - return applyPreset(doc.object()); -} - -QJsonObject PresetManager::buildPreset() const -{ - QJsonObject root; - root["version"] = QString("1.0"); - - const QVector nodes = m_controller->nodes(); - QHash nodesById; - for (const auto &node : nodes) { - nodesById.insert(node.id, node); - } - - QJsonArray virtualDevices; - for (const auto &node : nodes) { - if (node.type != Potato::NodeType::Virtual) { - continue; - } - QJsonObject device; - device["name"] = node.name; - device["description"] = node.description; - device["stable_id"] = node.stableId; - const int channels = std::max(node.inputPorts.size(), node.outputPorts.size()); - device["channels"] = channels > 0 ? channels : 2; - device["rate"] = 48000; - device["media_class"] = mediaClassToString(node.mediaClass); - virtualDevices.append(device); - } - root["virtual_devices"] = virtualDevices; - - QJsonArray routing; - const QVector links = m_controller->links(); - for (const auto &link : links) { - if (!nodesById.contains(link.outputNodeId) || !nodesById.contains(link.inputNodeId)) { - continue; - } - const Potato::NodeInfo &outNode = nodesById.value(link.outputNodeId); - const Potato::NodeInfo &inNode = nodesById.value(link.inputNodeId); - - QString outPortName; - QString inPortName; - if (!findPortName(outNode, link.outputPortId, outPortName)) { - continue; - } - if (!findPortName(inNode, link.inputPortId, inPortName)) { - continue; - } - - QJsonObject route; - route["source"] = QString("%1:%2").arg(outNode.stableId, outPortName); - route["target"] = QString("%1:%2").arg(inNode.stableId, inPortName); - route["volume"] = 1.0; - route["muted"] = false; - routing.append(route); - } - root["routing"] = routing; - - QJsonObject volumes; - QJsonObject mutes; - const QHash volumeStates = m_model->volumeStates(); - for (auto it = volumeStates.cbegin(); it != volumeStates.cend(); ++it) { - volumes[it.key()] = it.value().volume; - mutes[it.key()] = it.value().mute; - } - root["persistent_volumes"] = volumes; - root["persistent_mutes"] = mutes; - - root["ui_layout"] = m_model->layoutJson(); - - return root; -} - -bool PresetManager::applyPreset(const QJsonObject &root) -{ - const QVector nodes = m_controller->nodes(); - QHash nodesByStableId; - for (const auto &node : nodes) { - if (!node.stableId.isEmpty()) { - nodesByStableId.insert(node.stableId, node); - } - } - - const QJsonArray virtualDevices = root.value("virtual_devices").toArray(); - for (const auto &entry : virtualDevices) { - const QJsonObject device = entry.toObject(); - const QString stableId = device.value("stable_id").toString(); - const QString name = device.value("name").toString(); - const QString description = device.value("description").toString(); - const QString mediaClassValue = device.value("media_class").toString(); - const int channels = device.value("channels").toInt(2); - const int rate = device.value("rate").toInt(48000); - - if (!stableId.isEmpty() && nodesByStableId.contains(stableId)) { - continue; - } - - const Potato::MediaClass mediaClass = mediaClassFromString(mediaClassValue); - if (mediaClass == Potato::MediaClass::AudioSource) { - m_controller->createVirtualSource(name, description, channels, rate); - } else { - m_controller->createVirtualSink(name, description, channels, rate); - } - } - - const QJsonArray routing = root.value("routing").toArray(); - for (const auto &entry : routing) { - const QJsonObject route = entry.toObject(); - const QString source = route.value("source").toString(); - const QString target = route.value("target").toString(); - - uint32_t outNodeId = 0; - uint32_t outPortId = 0; - uint32_t inNodeId = 0; - uint32_t inPortId = 0; - - if (!parsePortKey(source, outNodeId, outPortId)) { - continue; - } - if (!parsePortKey(target, inNodeId, inPortId)) { - continue; - } - - m_controller->createLink(outNodeId, outPortId, inNodeId, inPortId); - } - - QHash volumeStates; - const QJsonObject volumes = root.value("persistent_volumes").toObject(); - const QJsonObject mutes = root.value("persistent_mutes").toObject(); - for (auto it = volumes.begin(); it != volumes.end(); ++it) { - NodeVolumeState state; - state.volume = static_cast(it.value().toDouble(1.0)); - state.mute = mutes.value(it.key()).toBool(false); - volumeStates.insert(it.key(), state); - } - if (!volumeStates.isEmpty()) { - m_model->applyVolumeStates(volumeStates); - } - - const QJsonObject layout = root.value("ui_layout").toObject(); - if (!layout.isEmpty()) { - m_model->applyLayoutJson(layout); - } - - return true; -} - -bool PresetManager::findPortName(const Potato::NodeInfo &node, uint32_t portId, QString &portName) const -{ - for (const auto &port : node.inputPorts) { - if (port.id == portId) { - portName = port.name; - return true; - } - } - for (const auto &port : node.outputPorts) { - if (port.id == portId) { - portName = port.name; - return true; - } - } - return false; -} - -bool PresetManager::parsePortKey(const QString &key, uint32_t &nodeId, uint32_t &portId) const -{ - const int split = key.lastIndexOf(':'); - if (split <= 0) { - return false; - } - - const QString stableId = key.left(split); - const QString portName = key.mid(split + 1); - if (stableId.isEmpty() || portName.isEmpty()) { - return false; - } - - const QVector nodes = m_controller->nodes(); - for (const auto &node : nodes) { - if (node.stableId != stableId) { - continue; - } - for (const auto &port : node.inputPorts) { - if (port.name == portName) { - nodeId = node.id; - portId = port.id; - return true; - } - } - for (const auto &port : node.outputPorts) { - if (port.name == portName) { - nodeId = node.id; - portId = port.id; - return true; - } - } - } - - return false; -} - -QString PresetManager::portKey(const Potato::NodeInfo &node, const Potato::PortInfo &port) const -{ - return QString("%1:%2").arg(node.stableId, port.name); -} - -QString PresetManager::mediaClassToString(Potato::MediaClass mediaClass) const -{ - switch (mediaClass) { - case Potato::MediaClass::AudioSink: - return QString("Audio/Sink"); - case Potato::MediaClass::AudioSource: - return QString("Audio/Source"); - case Potato::MediaClass::AudioDuplex: - return QString("Audio/Duplex"); - case Potato::MediaClass::Stream: - return QString("Stream"); - default: - return QString(); - } -} - -Potato::MediaClass PresetManager::mediaClassFromString(const QString &value) const -{ - if (value.contains("Audio/Source")) { - return Potato::MediaClass::AudioSource; - } - if (value.contains("Audio/Duplex")) { - return Potato::MediaClass::AudioDuplex; - } - if (value.contains("Audio/Sink")) { - return Potato::MediaClass::AudioSink; - } - if (value.contains("Stream")) { - return Potato::MediaClass::Stream; - } - return Potato::MediaClass::Unknown; -} diff --git a/src/presets/PresetManager.h b/src/presets/PresetManager.h deleted file mode 100644 index 13cb651..0000000 --- a/src/presets/PresetManager.h +++ /dev/null @@ -1,38 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -#include "pipewire/nodeinfo.h" - -class PipeWireGraphModel; - -namespace Potato { -class PipeWireController; -} - -class PresetManager : public QObject -{ -public: - explicit PresetManager(Potato::PipeWireController *controller, - PipeWireGraphModel *model, - QObject *parent = nullptr); - - bool savePreset(const QString &path) const; - bool loadPreset(const QString &path); - -private: - QJsonObject buildPreset() const; - bool applyPreset(const QJsonObject &root); - bool findPortName(const Potato::NodeInfo &node, uint32_t portId, QString &portName) const; - bool parsePortKey(const QString &key, uint32_t &nodeId, uint32_t &portId) const; - QString portKey(const Potato::NodeInfo &node, const Potato::PortInfo &port) const; - QString mediaClassToString(Potato::MediaClass mediaClass) const; - Potato::MediaClass mediaClassFromString(const QString &value) const; - - Potato::PipeWireController *m_controller = nullptr; - PipeWireGraphModel *m_model = nullptr; -};