diff --git a/CMakeLists.txt b/CMakeLists.txt index f383700..49afc73 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -83,8 +83,6 @@ if(WARPPIPE_BUILD_GUI) gui/WarpGraphModel.cpp gui/GraphEditorWidget.cpp gui/PresetManager.cpp - gui/VolumeWidgets.cpp - gui/AudioLevelMeter.cpp ) target_link_libraries(warppipe-gui PRIVATE @@ -100,8 +98,6 @@ if(WARPPIPE_BUILD_GUI) gui/WarpGraphModel.cpp gui/GraphEditorWidget.cpp gui/PresetManager.cpp - gui/VolumeWidgets.cpp - gui/AudioLevelMeter.cpp ) target_compile_definitions(warppipe-gui-tests PRIVATE WARPPIPE_TESTING) diff --git a/GUI_PLAN.md b/GUI_PLAN.md index 560715e..d2af7ad 100644 --- a/GUI_PLAN.md +++ b/GUI_PLAN.md @@ -231,63 +231,64 @@ A Qt6-based node editor GUI for warppipe using the QtNodes (nodeeditor) library. - [x] Add "Save Preset..." context menu action → `QFileDialog::getSaveFileName()` - [x] Add "Load Preset..." context menu action → `QFileDialog::getOpenFileName()` - [x] Add tests for preset save/load round-trip -- [x] Milestone 8d - Volume/Mute Controls (requires core API: `SetNodeVolume()`) - - [x] Add `NodeVolumeState` struct: `{ float volume; bool mute; }` - - [x] Add `ClickSlider : QSlider` — click jumps to position instead of page-stepping - - [x] Add inline volume widget per node via `nodeData(NodeRole::Widget)`: - - [x] Horizontal `ClickSlider` (0-100) + mute `QToolButton` - - [x] Calls `Client::SetNodeVolume(nodeId, volume, mute)` on change - - [x] Styled: dark background, green slider fill, rounded mute button - - [x] Implement `VolumeChangeCommand : QUndoCommand` - - [x] Stores previous + next `NodeVolumeState`, node ID - - [x] `undo()` → apply previous state; `redo()` → apply next state - - [x] Push on slider release or mute toggle (not during drag) - - [x] Track volume states in model: `std::unordered_map m_volumeStates` - - [x] `setNodeVolumeState()` — update state + sync inline widget + call Client API - - [x] `nodeVolumeState()` — read current state - - [x] Emit `nodeVolumeChanged(nodeId, previous, current)` signal - - [x] Add "MIXER" tab to sidebar `QTabWidget`: - - [x] `QScrollArea` with vertical layout of channel strips - - [x] Per-node strip: horizontal `ClickSlider` (fader) + Mute (M) button + node label - - [x] Volume fader changes push `VolumeChangeCommand` onto undo stack - - [x] `rebuildMixerStrips()` — create/remove strips when nodes appear/disappear - - [x] Mixer strips sync from model state via `nodeVolumeChanged` signal - - [x] Include volume/mute states in preset save/load (`volumes` array in JSON) - - [x] Add tests for volume state tracking, signal emission, widget sync, preset round-trip, cleanup on deletion -- [x] Milestone 8e - Audio Level Meters (requires core API: `MeterPeak()`, `NodeMeterPeak()`, `EnsureNodeMeter()`) - - [x] Implement `AudioLevelMeter : QWidget` - - [x] Custom `paintEvent`: vertical bar from bottom, background `(24,24,28)` - - [x] Color thresholds: green (0-0.7), yellow (0.7-0.9), red (0.9-1.0) - - [x] Peak hold indicator: white horizontal line, holds 6 frames then decays at 0.02/frame - - [x] `setLevel(float)` — clamp 0-1, update hold, call `update()` - - [x] `sizeHint()` → 40×160 - - [x] Add "METERS" tab to sidebar `QTabWidget`: - - [x] "MASTER OUTPUT" label + master `AudioLevelMeter` - - [x] "NODE METERS" label + scrollable list of per-node meter rows - - [x] Per-node row: elided label + compact `AudioLevelMeter` (fixed 26px wide, min 70px tall) - - [x] Add 30fps meter update timer (33ms, `Qt::PreciseTimer`) - - [x] Poll `Client::MeterPeak()` → master meter - - [x] Poll `Client::NodeMeterPeak(nodeId)` → per-node meters - - [x] Auto-rebuild node meters on node create/delete - - [x] Auto-manage per-node meters: - - [x] Call `EnsureNodeMeter()` for each node during rebuild - - [x] Remove meter rows when nodes deleted - - [x] `rebuildNodeMeters()` wired to `nodeCreated`/`nodeDeleted` signals - - [x] Add tests for AudioLevelMeter level clamping, hold/decay logic, METERS tab existence, meter row creation -- [x] Milestone 8f - Architecture and Routing Rules - - [x] Event-driven updates: core `SetChangeCallback()` fires on registry changes, GUI debounces via 50ms QTimer + QueuedConnection marshal (2s polling kept as fallback) - - [x] `Client::SetChangeCallback(ChangeCallback)` — fires from PW thread on node/port/link add/remove - - [x] `NotifyChange()` uses dedicated `change_cb_mutex` (not cache_mutex) to avoid lock ordering issues - - [x] GUI marshals to Qt thread via `QMetaObject::invokeMethod(..., Qt::QueuedConnection)` - - [x] Link intent system: implemented via core `saved_links` + deferred `ProcessSavedLinks()` - - [x] `LoadConfig()` parses links into `saved_links` vector (stable node:port name pairs) - - [x] `ProcessSavedLinks()` resolves names → port IDs on each CoreDone, creates via `CreateSavedLinkAsync()` - - [x] Competing links from WirePlumber auto-removed after saved link creation - - [x] Persisted in config.json `links` array (not layout JSON — core owns link state) - - [x] Add routing rule UI (RULES sidebar tab) - - [x] List existing rules from `Client::ListRouteRules()` as styled cards - - [x] Add rules via dialog with Application Name, Process Binary, Media Role, Target Node fields - - [x] Delete rules via per-card ✕ button +- [ ] Milestone 8d - Volume/Mute Controls (requires core API: `SetNodeVolume()`) + - [ ] Add `NodeVolumeState` struct: `{ float volume; bool mute; }` + - [ ] Add `ClickSlider : QSlider` — click jumps to position instead of page-stepping + - [ ] Add inline volume widget per node via `nodeData(NodeRole::Widget)`: + - [ ] Horizontal `ClickSlider` (0-100) + mute `QToolButton` + - [ ] Calls `Client::SetNodeVolume(nodeId, volume, mute)` on change + - [ ] Styled: dark background, green slider fill, rounded mute button + - [ ] Implement `VolumeChangeCommand : QUndoCommand` + - [ ] Stores previous + next `NodeVolumeState`, node ID + - [ ] `undo()` → apply previous state; `redo()` → apply next state + - [ ] Push on slider release or mute toggle (not during drag) + - [ ] Track volume states in model: `QHash m_nodeVolumeState` + - [ ] `setNodeVolumeState()` — update state + sync inline widget + - [ ] `nodeVolumeState()` — read current state + - [ ] Emit `nodeVolumeChanged(nodeId, previous, current)` signal + - [ ] Add "MIXER" tab to sidebar `QTabWidget`: + - [ ] `QScrollArea` with horizontal layout of channel strips + - [ ] Per-node strip: `AudioLevelMeter` + vertical `ClickSlider` (fader) + Mute (M) + Solo (S) buttons + node label + - [ ] Solo logic: when any node is soloed, all non-soloed nodes are muted + - [ ] Volume fader changes push `VolumeChangeCommand` onto undo stack + - [ ] `refreshMixerStrip()` — create strip when node appears + - [ ] `removeMixerStrip()` — destroy strip when node removed + - [ ] `updateMixerState()` — sync fader/mute from model state + - [ ] Include volume/mute states in preset save/load (`persistent_volumes`, `persistent_mutes`) + - [ ] Add tests for VolumeChangeCommand undo/redo and mixer strip lifecycle +- [ ] Milestone 8e - Audio Level Meters (requires core API: `MeterPeak()`, `NodeMeterPeak()`, `EnsureNodeMeter()`) + - [ ] Implement `AudioLevelMeter : QWidget` + - [ ] Custom `paintEvent`: vertical bar from bottom, background `(24,24,28)` + - [ ] Color thresholds: green (0-0.7), yellow (0.7-0.9), red (0.9-1.0) + - [ ] Peak hold indicator: white horizontal line, holds 6 frames then decays at 0.02/frame + - [ ] `setLevel(float)` — clamp 0-1, update hold, call `update()` + - [ ] `sizeHint()` → 40×160 + - [ ] Add "METERS" tab to sidebar `QTabWidget`: + - [ ] "MASTER OUTPUT" label + master `AudioLevelMeter` + - [ ] "NODE METERS" label + scrollable list of per-node meter rows + - [ ] Per-node row: elided label + compact `AudioLevelMeter` (fixed 26px wide, min 70px tall) + - [ ] Add 30fps meter update timer (33ms, `Qt::PreciseTimer`) + - [ ] Poll `Client::MeterPeak()` → master meter + - [ ] Poll `Client::NodeMeterPeak(nodeId)` → per-node meters + mixer meters + - [ ] Skip updates when widget is not visible (`isVisible()` check) + - [ ] Auto-manage per-node meters: + - [ ] Create meter when node has active links (`ensureNodeMeter()`) + - [ ] Remove meter when node removed or all links removed (`removeNodeMeter()`) + - [ ] Skip meter nodes (filter by name prefix) + - [ ] Add tests for AudioLevelMeter level clamping, hold/decay logic +- [ ] Milestone 8f - Architecture and Routing Rules + - [ ] Event-driven updates: replace 500ms polling with signal/slot if core adds registry callbacks + - [ ] `nodeAdded(NodeInfo)`, `nodeRemoved(uint32_t)`, `nodeChanged(NodeInfo)` + - [ ] `linkAdded(LinkInfo)`, `linkRemoved(uint32_t)` + - [ ] Keep polling as fallback if signals not available + - [ ] Link intent system: remember intended links by stable key, restore when nodes reappear + - [ ] `rememberLinkIntent(LinkInfo)` — store stable_id:port_name pairs + - [ ] `tryRestoreLinks()` — called on node add, resolves stored intents + - [ ] Persist link intents in layout JSON + - [ ] Add routing rule UI (separate panel or dialog) + - [ ] List existing rules from `Client::ListRouteRules()` + - [ ] Add/remove rules with RuleMatch fields + - [ ] Show which nodes are affected by rules --- diff --git a/gui/AudioLevelMeter.cpp b/gui/AudioLevelMeter.cpp deleted file mode 100644 index c4b9a8e..0000000 --- a/gui/AudioLevelMeter.cpp +++ /dev/null @@ -1,90 +0,0 @@ -#include "AudioLevelMeter.h" - -#include - -#include - -AudioLevelMeter::AudioLevelMeter(QWidget *parent) : QWidget(parent) { - setAutoFillBackground(false); - setAttribute(Qt::WA_OpaquePaintEvent); -} - -void AudioLevelMeter::setLevel(float level) { - m_level = std::clamp(level, 0.0f, 1.0f); - - if (m_level >= m_peakHold) { - m_peakHold = m_level; - m_peakHoldFrames = 0; - } else { - ++m_peakHoldFrames; - if (m_peakHoldFrames > kPeakHoldDuration) { - m_peakHold = std::max(0.0f, m_peakHold - kPeakDecayRate); - } - } - - update(); -} - -float AudioLevelMeter::level() const { return m_level; } - -float AudioLevelMeter::peakHold() const { return m_peakHold; } - -void AudioLevelMeter::resetPeakHold() { - m_peakHold = 0.0f; - m_peakHoldFrames = 0; - update(); -} - -QSize AudioLevelMeter::sizeHint() const { return {40, 160}; } - -QSize AudioLevelMeter::minimumSizeHint() const { return {12, 40}; } - -void AudioLevelMeter::paintEvent(QPaintEvent *) { - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing, false); - - QRect r = rect(); - painter.fillRect(r, QColor(24, 24, 28)); - - if (m_level <= 0.0f && m_peakHold <= 0.0f) - return; - - int barHeight = static_cast(m_level * r.height()); - int barTop = r.height() - barHeight; - - if (barHeight > 0) { - float greenEnd = 0.7f * r.height(); - float yellowEnd = 0.9f * r.height(); - - int greenH = std::min(barHeight, static_cast(greenEnd)); - if (greenH > 0) { - painter.fillRect(r.left(), r.bottom() - greenH + 1, r.width(), greenH, - QColor(76, 175, 80)); - } - - if (barHeight > static_cast(greenEnd)) { - int yellowH = - std::min(barHeight - static_cast(greenEnd), - static_cast(yellowEnd) - static_cast(greenEnd)); - if (yellowH > 0) { - painter.fillRect(r.left(), r.bottom() - static_cast(greenEnd) - yellowH + 1, - r.width(), yellowH, QColor(255, 193, 7)); - } - } - - if (barHeight > static_cast(yellowEnd)) { - int redH = barHeight - static_cast(yellowEnd); - if (redH > 0) { - painter.fillRect(r.left(), barTop, r.width(), redH, - QColor(244, 67, 54)); - } - } - } - - if (m_peakHold > 0.0f) { - int peakY = r.height() - static_cast(m_peakHold * r.height()); - peakY = std::clamp(peakY, r.top(), r.bottom()); - painter.setPen(QColor(255, 255, 255)); - painter.drawLine(r.left(), peakY, r.right(), peakY); - } -} diff --git a/gui/AudioLevelMeter.h b/gui/AudioLevelMeter.h deleted file mode 100644 index b9ef1e1..0000000 --- a/gui/AudioLevelMeter.h +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once - -#include - -class AudioLevelMeter : public QWidget { - Q_OBJECT -public: - explicit AudioLevelMeter(QWidget *parent = nullptr); - - void setLevel(float level); - float level() const; - float peakHold() const; - void resetPeakHold(); - - QSize sizeHint() const override; - QSize minimumSizeHint() const override; - -protected: - void paintEvent(QPaintEvent *event) override; - -private: - float m_level = 0.0f; - float m_peakHold = 0.0f; - int m_peakHoldFrames = 0; - - static constexpr int kPeakHoldDuration = 6; - static constexpr float kPeakDecayRate = 0.02f; -}; diff --git a/gui/GraphEditorWidget.cpp b/gui/GraphEditorWidget.cpp index e05eef2..72c7a9d 100644 --- a/gui/GraphEditorWidget.cpp +++ b/gui/GraphEditorWidget.cpp @@ -1,7 +1,5 @@ -#include "AudioLevelMeter.h" #include "GraphEditorWidget.h" #include "PresetManager.h" -#include "VolumeWidgets.h" #include "WarpGraphModel.h" #include @@ -14,18 +12,13 @@ #include #include #include -#include #include #include -#include -#include #include #include -#include #include #include #include -#include #include #include #include @@ -36,7 +29,6 @@ #include #include #include -#include #include #include #include @@ -128,32 +120,6 @@ private: std::vector m_snapshots; }; -class VolumeChangeCommand : public QUndoCommand { -public: - VolumeChangeCommand(WarpGraphModel *model, QtNodes::NodeId nodeId, - WarpGraphModel::NodeVolumeState previous, - WarpGraphModel::NodeVolumeState next) - : m_model(model), m_nodeId(nodeId), m_previous(previous), m_next(next) { - setText(QStringLiteral("Volume Change")); - } - - void undo() override { - if (m_model) - m_model->setNodeVolumeState(m_nodeId, m_previous); - } - - void redo() override { - if (m_model) - m_model->setNodeVolumeState(m_nodeId, m_next); - } - -private: - WarpGraphModel *m_model = nullptr; - QtNodes::NodeId m_nodeId; - WarpGraphModel::NodeVolumeState m_previous; - WarpGraphModel::NodeVolumeState m_next; -}; - GraphEditorWidget::GraphEditorWidget(warppipe::Client *client, QWidget *parent) : QWidget(parent), m_client(client) { @@ -222,85 +188,8 @@ GraphEditorWidget::GraphEditorWidget(warppipe::Client *client, presetsLayout->addWidget(loadPresetBtn); presetsLayout->addStretch(); - auto *metersTab = new QWidget(); - auto *metersLayout = new QVBoxLayout(metersTab); - metersLayout->setContentsMargins(8, 8, 8, 8); - metersLayout->setSpacing(8); - - auto *masterLabel = new QLabel(QStringLiteral("MASTER OUTPUT")); - masterLabel->setStyleSheet(QStringLiteral( - "QLabel { color: #a0a8b6; font-size: 11px; font-weight: bold;" - " background: transparent; }")); - metersLayout->addWidget(masterLabel); - - auto *masterRow = new QWidget(); - auto *masterRowLayout = new QHBoxLayout(masterRow); - masterRowLayout->setContentsMargins(0, 0, 0, 0); - masterRowLayout->setSpacing(4); - m_masterMeterL = new AudioLevelMeter(); - m_masterMeterL->setFixedWidth(18); - m_masterMeterL->setMinimumHeight(100); - m_masterMeterR = new AudioLevelMeter(); - m_masterMeterR->setFixedWidth(18); - m_masterMeterR->setMinimumHeight(100); - masterRowLayout->addStretch(); - masterRowLayout->addWidget(m_masterMeterL); - masterRowLayout->addWidget(m_masterMeterR); - masterRowLayout->addStretch(); - metersLayout->addWidget(masterRow); - - auto *nodeMetersLabel = new QLabel(QStringLiteral("NODE METERS")); - nodeMetersLabel->setStyleSheet(masterLabel->styleSheet()); - metersLayout->addWidget(nodeMetersLabel); - - m_nodeMeterScroll = new QScrollArea(); - m_nodeMeterScroll->setWidgetResizable(true); - m_nodeMeterScroll->setStyleSheet(QStringLiteral( - "QScrollArea { background: transparent; border: none; }" - "QScrollBar:vertical { background: #1a1a1e; width: 8px; }" - "QScrollBar::handle:vertical { background: #3a3a44; border-radius: 4px; }" - "QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { height: 0; }")); - m_nodeMeterContainer = new QWidget(); - m_nodeMeterContainer->setStyleSheet(QStringLiteral("background: transparent;")); - auto *nodeMeterLayout = new QVBoxLayout(m_nodeMeterContainer); - nodeMeterLayout->setContentsMargins(0, 0, 0, 0); - nodeMeterLayout->setSpacing(2); - nodeMeterLayout->addStretch(); - m_nodeMeterScroll->setWidget(m_nodeMeterContainer); - metersLayout->addWidget(m_nodeMeterScroll, 1); - - metersTab->setStyleSheet(QStringLiteral("background: #1a1a1e;")); - m_sidebar->addTab(metersTab, QStringLiteral("METERS")); - - m_mixerScroll = new QScrollArea(); - m_mixerScroll->setWidgetResizable(true); - m_mixerScroll->setStyleSheet(QStringLiteral( - "QScrollArea { background: #1a1a1e; border: none; }" - "QScrollBar:vertical { background: #1a1a1e; width: 8px; }" - "QScrollBar::handle:vertical { background: #3a3a44; border-radius: 4px; }" - "QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { height: 0; }")); - m_mixerContainer = new QWidget(); - m_mixerContainer->setStyleSheet(QStringLiteral("background: #1a1a1e;")); - auto *mixerLayout = new QVBoxLayout(m_mixerContainer); - mixerLayout->setContentsMargins(4, 4, 4, 4); - mixerLayout->setSpacing(2); - mixerLayout->addStretch(); - m_mixerScroll->setWidget(m_mixerContainer); - m_sidebar->addTab(m_mixerScroll, QStringLiteral("MIXER")); m_sidebar->addTab(presetsTab, QStringLiteral("PRESETS")); - m_rulesScroll = new QScrollArea(); - m_rulesScroll->setWidgetResizable(true); - m_rulesScroll->setStyleSheet(m_mixerScroll->styleSheet()); - m_rulesContainer = new QWidget(); - m_rulesContainer->setStyleSheet(QStringLiteral("background: #1a1a1e;")); - auto *rulesLayout = new QVBoxLayout(m_rulesContainer); - rulesLayout->setContentsMargins(8, 8, 8, 8); - rulesLayout->setSpacing(6); - rulesLayout->addStretch(); - m_rulesScroll->setWidget(m_rulesContainer); - m_sidebar->addTab(m_rulesScroll, QStringLiteral("RULES")); - m_splitter = new QSplitter(Qt::Horizontal); m_splitter->addWidget(m_view); m_splitter->addWidget(m_sidebar); @@ -416,22 +305,6 @@ GraphEditorWidget::GraphEditorWidget(warppipe::Client *client, connect(m_model, &QtNodes::AbstractGraphModel::nodeUpdated, this, &GraphEditorWidget::scheduleSaveLayout); - connect(m_model, &QtNodes::AbstractGraphModel::nodeCreated, this, - [this](QtNodes::NodeId nodeId) { - wireVolumeWidget(nodeId); - rebuildMixerStrips(); - rebuildNodeMeters(); - rebuildRulesList(); - }); - connect(m_model, &QtNodes::AbstractGraphModel::nodeDeleted, this, - [this](QtNodes::NodeId nodeId) { - m_mixerStrips.erase(nodeId); - m_nodeMeters.erase(nodeId); - rebuildMixerStrips(); - rebuildNodeMeters(); - rebuildRulesList(); - }); - m_saveTimer = new QTimer(this); m_saveTimer->setSingleShot(true); m_saveTimer->setInterval(1000); @@ -439,7 +312,6 @@ GraphEditorWidget::GraphEditorWidget(warppipe::Client *client, &GraphEditorWidget::saveLayoutWithViewState); m_model->refreshFromClient(); - rebuildRulesList(); if (!hasLayout) { m_model->autoArrange(); } @@ -454,30 +326,10 @@ GraphEditorWidget::GraphEditorWidget(warppipe::Client *client, connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, &GraphEditorWidget::saveLayoutWithViewState); - m_changeTimer = new QTimer(this); - m_changeTimer->setSingleShot(true); - m_changeTimer->setInterval(50); - connect(m_changeTimer, &QTimer::timeout, this, - &GraphEditorWidget::onRefreshTimer); - - if (m_client) { - m_client->SetChangeCallback([this] { - QMetaObject::invokeMethod(m_changeTimer, - qOverload<>(&QTimer::start), - Qt::QueuedConnection); - }); - } - m_refreshTimer = new QTimer(this); connect(m_refreshTimer, &QTimer::timeout, this, &GraphEditorWidget::onRefreshTimer); - m_refreshTimer->start(2000); - - m_meterTimer = new QTimer(this); - m_meterTimer->setTimerType(Qt::PreciseTimer); - connect(m_meterTimer, &QTimer::timeout, this, - &GraphEditorWidget::updateMeters); - m_meterTimer->start(33); + m_refreshTimer->start(500); } void GraphEditorWidget::onRefreshTimer() { @@ -496,12 +348,6 @@ void GraphEditorWidget::scheduleSaveLayout() { } } -GraphEditorWidget::~GraphEditorWidget() { - if (m_client) { - m_client->SetChangeCallback(nullptr); - } -} - int GraphEditorWidget::nodeCount() const { return static_cast(m_model->allNodeIds().size()); } @@ -718,13 +564,6 @@ void GraphEditorWidget::showNodeContextMenu(const QPoint &screenPos, deleteAction->setShortcut(QKeySequence::Delete); } - QAction *createRuleAction = nullptr; - if (type == WarpNodeType::kApplication) { - menu.addSeparator(); - createRuleAction = menu.addAction(QStringLiteral("Create Rule...")); - } - - menu.addSeparator(); QAction *pasteAction = menu.addAction(QStringLiteral("Paste")); pasteAction->setShortcut(QKeySequence::Paste); pasteAction->setEnabled(!m_clipboardJson.isEmpty() || @@ -745,10 +584,6 @@ void GraphEditorWidget::showNodeContextMenu(const QPoint &screenPos, deleteSelection(); } else if (chosen == pasteAction) { pasteSelection(QPointF(0, 0)); - } else if (chosen == createRuleAction) { - showAddRuleDialog(data->info.application_name, - data->info.process_binary, - data->info.media_role); } } @@ -1085,7 +920,7 @@ void GraphEditorWidget::tryResolvePendingLinks() { } if (foundOut && foundIn) { - m_client->CreateLink(outPortId, inPortId, warppipe::LinkOptions{.linger = true}); + m_client->CreateLink(outPortId, inPortId, warppipe::LinkOptions{}); } else { remaining.push_back(pending); } @@ -1159,458 +994,3 @@ void GraphEditorWidget::loadPreset() { QStringLiteral("Failed to load preset.")); } } - -void GraphEditorWidget::wireVolumeWidget(QtNodes::NodeId nodeId) { - auto widget = - m_model->nodeData(nodeId, QtNodes::NodeRole::Widget); - auto *w = widget.value(); - auto *vol = qobject_cast(w); - if (!vol) - return; - - auto capturedId = nodeId; - - connect(vol, &NodeVolumeWidget::volumeChanged, this, - [this, capturedId](int value) { - auto state = m_model->nodeVolumeState(capturedId); - state.volume = static_cast(value) / 100.0f; - m_model->setNodeVolumeState(capturedId, state); - }); - - connect(vol, &NodeVolumeWidget::sliderReleased, this, - [this, capturedId, vol]() { - auto current = m_model->nodeVolumeState(capturedId); - WarpGraphModel::NodeVolumeState previous; - previous.volume = current.volume; - previous.mute = current.mute; - m_scene->undoStack().push( - new VolumeChangeCommand(m_model, capturedId, previous, current)); - }); - - connect(vol, &NodeVolumeWidget::muteToggled, this, - [this, capturedId](bool muted) { - auto previous = m_model->nodeVolumeState(capturedId); - auto next = previous; - next.mute = muted; - m_model->setNodeVolumeState(capturedId, next); - m_scene->undoStack().push( - new VolumeChangeCommand(m_model, capturedId, previous, next)); - }); -} - -void GraphEditorWidget::rebuildMixerStrips() { - if (!m_mixerContainer) - return; - - auto *layout = m_mixerContainer->layout(); - if (!layout) - return; - - while (layout->count() > 0) { - auto *item = layout->takeAt(0); - if (item->widget()) - item->widget()->deleteLater(); - delete item; - } - m_mixerStrips.clear(); - - auto nodeIds = m_model->allNodeIds(); - std::vector sorted(nodeIds.begin(), nodeIds.end()); - std::sort(sorted.begin(), sorted.end()); - - for (auto nodeId : sorted) { - const WarpNodeData *data = m_model->warpNodeData(nodeId); - if (!data) - continue; - - auto *strip = new QWidget(); - strip->setStyleSheet(QStringLiteral( - "QWidget { background: #24242a; border-radius: 4px; }")); - - auto *stripLayout = new QHBoxLayout(strip); - stripLayout->setContentsMargins(6, 4, 6, 4); - stripLayout->setSpacing(6); - - auto *label = new QLabel( - WarpGraphModel::classifyNode(data->info) == WarpNodeType::kApplication - ? QString::fromStdString( - data->info.application_name.empty() - ? data->info.name - : data->info.application_name) - : QString::fromStdString( - data->info.description.empty() - ? data->info.name - : data->info.description)); - label->setFixedWidth(120); - label->setStyleSheet(QStringLiteral( - "QLabel { color: #a0a8b6; font-size: 11px; background: transparent; }")); - label->setToolTip(QString::fromStdString(data->info.name)); - - auto *slider = new ClickSlider(Qt::Horizontal); - slider->setRange(0, 100); - auto state = m_model->nodeVolumeState(nodeId); - slider->setValue(static_cast(state.volume * 100.0f)); - slider->setStyleSheet(QStringLiteral( - "QSlider::groove:horizontal {" - " background: #1a1a1e; border-radius: 3px; height: 6px; }" - "QSlider::handle:horizontal {" - " background: #ecf0f6; border-radius: 5px;" - " width: 10px; margin: -4px 0; }" - "QSlider::sub-page:horizontal {" - " background: #4caf50; border-radius: 3px; }")); - - auto *muteBtn = new QToolButton(); - muteBtn->setText(QStringLiteral("M")); - muteBtn->setCheckable(true); - muteBtn->setChecked(state.mute); - muteBtn->setFixedSize(22, 22); - muteBtn->setStyleSheet(QStringLiteral( - "QToolButton {" - " background: #2e2e36; color: #ecf0f6; border: 1px solid #3a3a44;" - " border-radius: 4px; font-weight: bold; font-size: 11px; }" - "QToolButton:checked {" - " background: #b03030; color: #ecf0f6; border: 1px solid #d04040; }" - "QToolButton:hover { background: #3a3a44; }" - "QToolButton:checked:hover { background: #c04040; }")); - - stripLayout->addWidget(label); - stripLayout->addWidget(slider, 1); - stripLayout->addWidget(muteBtn); - - auto capturedId = nodeId; - - connect(slider, &QSlider::valueChanged, this, - [this, capturedId](int value) { - auto s = m_model->nodeVolumeState(capturedId); - s.volume = static_cast(value) / 100.0f; - m_model->setNodeVolumeState(capturedId, s); - }); - - connect(slider, &QSlider::sliderReleased, this, - [this, capturedId]() { - auto current = m_model->nodeVolumeState(capturedId); - m_scene->undoStack().push( - new VolumeChangeCommand(m_model, capturedId, current, current)); - }); - - connect(muteBtn, &QToolButton::toggled, this, - [this, capturedId](bool muted) { - auto prev = m_model->nodeVolumeState(capturedId); - auto next = prev; - next.mute = muted; - m_model->setNodeVolumeState(capturedId, next); - m_scene->undoStack().push( - new VolumeChangeCommand(m_model, capturedId, prev, next)); - }); - - connect(m_model, &WarpGraphModel::nodeVolumeChanged, slider, - [slider, muteBtn, capturedId](QtNodes::NodeId id, - WarpGraphModel::NodeVolumeState, - WarpGraphModel::NodeVolumeState cur) { - if (id != capturedId) - return; - QSignalBlocker sb(slider); - QSignalBlocker mb(muteBtn); - slider->setValue(static_cast(cur.volume * 100.0f)); - muteBtn->setChecked(cur.mute); - }); - - layout->addWidget(strip); - m_mixerStrips[nodeId] = strip; - } - - static_cast(layout)->addStretch(); -} - -void GraphEditorWidget::updateMeters() { - if (!m_client) - return; - - auto master = m_client->MeterPeak(); - if (master.ok()) { - m_masterMeterL->setLevel(master.value.peak_left); - m_masterMeterR->setLevel(master.value.peak_right); - } - - for (auto &[nodeId, row] : m_nodeMeters) { - const WarpNodeData *data = m_model->warpNodeData(nodeId); - if (!data || !row.meter) - continue; - auto peak = m_client->NodeMeterPeak(data->info.id); - if (peak.ok()) { - row.meter->setLevel( - std::max(peak.value.peak_left, peak.value.peak_right)); - } - } -} - -void GraphEditorWidget::rebuildNodeMeters() { - if (!m_nodeMeterContainer || !m_client) - return; - - auto *layout = m_nodeMeterContainer->layout(); - if (!layout) - return; - - std::unordered_map old_pw_ids; - for (const auto &[nid, row] : m_nodeMeters) { - const WarpNodeData *d = m_model->warpNodeData(nid); - if (d) - old_pw_ids[d->info.id.value] = true; - } - - while (layout->count() > 0) { - auto *item = layout->takeAt(0); - if (item->widget()) - item->widget()->deleteLater(); - delete item; - } - m_nodeMeters.clear(); - - auto nodeIds = m_model->allNodeIds(); - std::vector sorted(nodeIds.begin(), nodeIds.end()); - std::sort(sorted.begin(), sorted.end()); - - std::unordered_map new_pw_ids; - for (auto nodeId : sorted) { - const WarpNodeData *data = m_model->warpNodeData(nodeId); - if (!data) - continue; - - new_pw_ids[data->info.id.value] = true; - m_client->EnsureNodeMeter(data->info.id); - - auto *row = new QWidget(); - auto *rowLayout = new QHBoxLayout(row); - rowLayout->setContentsMargins(0, 0, 0, 0); - rowLayout->setSpacing(6); - - auto *label = new QLabel( - WarpGraphModel::classifyNode(data->info) == WarpNodeType::kApplication - ? QString::fromStdString( - data->info.application_name.empty() - ? data->info.name - : data->info.application_name) - : QString::fromStdString( - data->info.description.empty() - ? data->info.name - : data->info.description)); - label->setStyleSheet(QStringLiteral( - "QLabel { color: #a0a8b6; font-size: 11px; background: transparent; }")); - label->setToolTip(QString::fromStdString(data->info.name)); - - auto *meter = new AudioLevelMeter(); - meter->setFixedWidth(26); - meter->setMinimumHeight(70); - - rowLayout->addWidget(label, 1); - rowLayout->addWidget(meter); - - layout->addWidget(row); - - NodeMeterRow meterRow; - meterRow.widget = row; - meterRow.meter = meter; - meterRow.label = label; - m_nodeMeters[nodeId] = meterRow; - } - - static_cast(layout)->addStretch(); - - for (const auto &[pw_id, _] : old_pw_ids) { - if (new_pw_ids.find(pw_id) == new_pw_ids.end()) { - m_client->DisableNodeMeter(warppipe::NodeId{pw_id}); - } - } -} - -void GraphEditorWidget::rebuildRulesList() { - if (!m_rulesContainer || !m_client) - return; - - auto *layout = m_rulesContainer->layout(); - if (!layout) - return; - - while (layout->count() > 0) { - auto *item = layout->takeAt(0); - if (item->widget()) - item->widget()->deleteLater(); - delete item; - } - - const QString labelStyle = QStringLiteral( - "QLabel { color: #a0a8b6; font-size: 11px; background: transparent; }"); - const QString valueStyle = QStringLiteral( - "QLabel { color: #ecf0f6; font-size: 12px; background: transparent; }"); - const QString btnStyle = QStringLiteral( - "QPushButton { background: #2e2e36; color: #ecf0f6; border: 1px solid #3a3a44;" - " border-radius: 4px; padding: 6px 12px; }" - "QPushButton:hover { background: #3a3a44; }" - "QPushButton:pressed { background: #44444e; }"); - const QString delBtnStyle = QStringLiteral( - "QPushButton { background: transparent; color: #a05050; border: none;" - " font-size: 14px; font-weight: bold; padding: 2px 6px; }" - "QPushButton:hover { color: #e05050; }"); - - auto *header = new QLabel(QStringLiteral("ROUTING RULES")); - header->setStyleSheet(QStringLiteral( - "QLabel { color: #a0a8b6; font-size: 11px; font-weight: bold;" - " background: transparent; }")); - layout->addWidget(header); - - auto rulesResult = m_client->ListRouteRules(); - if (rulesResult.ok()) { - for (const auto &rule : rulesResult.value) { - auto *card = new QWidget(); - card->setStyleSheet(QStringLiteral( - "QWidget { background: #24242a; border-radius: 4px; }")); - auto *cardLayout = new QHBoxLayout(card); - cardLayout->setContentsMargins(8, 6, 4, 6); - cardLayout->setSpacing(8); - - QString matchText; - if (!rule.match.application_name.empty()) - matchText += QStringLiteral("app: ") + - QString::fromStdString(rule.match.application_name); - if (!rule.match.process_binary.empty()) { - if (!matchText.isEmpty()) matchText += QStringLiteral(", "); - matchText += QStringLiteral("bin: ") + - QString::fromStdString(rule.match.process_binary); - } - if (!rule.match.media_role.empty()) { - if (!matchText.isEmpty()) matchText += QStringLiteral(", "); - matchText += QStringLiteral("role: ") + - QString::fromStdString(rule.match.media_role); - } - - auto *infoLayout = new QVBoxLayout(); - infoLayout->setContentsMargins(0, 0, 0, 0); - infoLayout->setSpacing(2); - - auto *matchLabel = new QLabel(matchText); - matchLabel->setStyleSheet(valueStyle); - infoLayout->addWidget(matchLabel); - - auto *targetLabel = new QLabel( - QString(QChar(0x2192)) + QStringLiteral(" ") + - QString::fromStdString(rule.target_node)); - targetLabel->setStyleSheet(labelStyle); - infoLayout->addWidget(targetLabel); - - cardLayout->addLayout(infoLayout, 1); - - auto *delBtn = new QPushButton(QString(QChar(0x2715))); - delBtn->setFixedSize(24, 24); - delBtn->setStyleSheet(delBtnStyle); - warppipe::RuleId ruleId = rule.id; - connect(delBtn, &QPushButton::clicked, this, [this, ruleId]() { - m_client->RemoveRouteRule(ruleId); - rebuildRulesList(); - }); - cardLayout->addWidget(delBtn); - - layout->addWidget(card); - } - } - - auto *addBtn = new QPushButton(QStringLiteral("Add Rule...")); - addBtn->setStyleSheet(btnStyle); - connect(addBtn, &QPushButton::clicked, this, - [this]() { showAddRuleDialog(); }); - layout->addWidget(addBtn); - - static_cast(layout)->addStretch(); -} - -void GraphEditorWidget::showAddRuleDialog(const std::string &prefillApp, - const std::string &prefillBin, - const std::string &prefillRole) { - if (!m_client) - return; - - QDialog dlg(this); - dlg.setWindowTitle(QStringLiteral("Add Routing Rule")); - dlg.setStyleSheet(QStringLiteral( - "QDialog { background: #1e1e22; }" - "QLabel { color: #ecf0f6; }" - "QLineEdit { background: #2a2a32; color: #ecf0f6; border: 1px solid #3a3a44;" - " border-radius: 4px; padding: 4px 8px; }" - "QComboBox { background: #2a2a32; color: #ecf0f6; border: 1px solid #3a3a44;" - " border-radius: 4px; padding: 4px 8px; }" - "QComboBox::drop-down { border: none; }" - "QComboBox QAbstractItemView { background: #2a2a32; color: #ecf0f6;" - " selection-background-color: #3a3a44; }")); - - auto *form = new QFormLayout(&dlg); - form->setContentsMargins(16, 16, 16, 16); - form->setSpacing(8); - - auto *appNameEdit = new QLineEdit(); - appNameEdit->setPlaceholderText(QStringLiteral("e.g. Firefox")); - if (!prefillApp.empty()) - appNameEdit->setText(QString::fromStdString(prefillApp)); - form->addRow(QStringLiteral("Application Name:"), appNameEdit); - - auto *processBinEdit = new QLineEdit(); - processBinEdit->setPlaceholderText(QStringLiteral("e.g. firefox")); - if (!prefillBin.empty()) - processBinEdit->setText(QString::fromStdString(prefillBin)); - form->addRow(QStringLiteral("Process Binary:"), processBinEdit); - - auto *mediaRoleEdit = new QLineEdit(); - mediaRoleEdit->setPlaceholderText(QStringLiteral("e.g. Music")); - if (!prefillRole.empty()) - mediaRoleEdit->setText(QString::fromStdString(prefillRole)); - form->addRow(QStringLiteral("Media Role:"), mediaRoleEdit); - - auto *targetCombo = new QComboBox(); - auto nodesResult = m_client->ListNodes(); - if (nodesResult.ok()) { - for (const auto &node : nodesResult.value) { - if (node.media_class.find("Sink") != std::string::npos) { - QString label = QString::fromStdString( - node.description.empty() ? node.name : node.description); - targetCombo->addItem(label, QString::fromStdString(node.name)); - } - } - } - form->addRow(QStringLiteral("Target Node:"), targetCombo); - - auto *buttons = new QDialogButtonBox( - QDialogButtonBox::Ok | QDialogButtonBox::Cancel); - buttons->setStyleSheet(QStringLiteral( - "QPushButton { background: #2e2e36; color: #ecf0f6; border: 1px solid #3a3a44;" - " border-radius: 4px; padding: 6px 16px; }" - "QPushButton:hover { background: #3a3a44; }")); - connect(buttons, &QDialogButtonBox::accepted, &dlg, &QDialog::accept); - connect(buttons, &QDialogButtonBox::rejected, &dlg, &QDialog::reject); - form->addRow(buttons); - - if (dlg.exec() != QDialog::Accepted) - return; - - std::string appName = appNameEdit->text().trimmed().toStdString(); - std::string procBin = processBinEdit->text().trimmed().toStdString(); - std::string role = mediaRoleEdit->text().trimmed().toStdString(); - std::string target = targetCombo->currentData().toString().toStdString(); - - if (appName.empty() && procBin.empty() && role.empty()) { - QMessageBox::warning(this, QStringLiteral("Invalid Rule"), - QStringLiteral("At least one match field must be filled.")); - return; - } - if (target.empty()) { - QMessageBox::warning(this, QStringLiteral("Invalid Rule"), - QStringLiteral("A target node must be selected.")); - return; - } - - warppipe::RouteRule rule; - rule.match.application_name = appName; - rule.match.process_binary = procBin; - rule.match.media_role = role; - rule.target_node = target; - m_client->AddRouteRule(rule); - rebuildRulesList(); -} diff --git a/gui/GraphEditorWidget.h b/gui/GraphEditorWidget.h index deec287..d70bdaa 100644 --- a/gui/GraphEditorWidget.h +++ b/gui/GraphEditorWidget.h @@ -8,7 +8,6 @@ #include #include -#include #include namespace QtNodes { @@ -17,11 +16,8 @@ class BasicGraphicsScene; class GraphicsView; } // namespace QtNodes -class AudioLevelMeter; class WarpGraphModel; -class NodeVolumeWidget; class QLabel; -class QScrollArea; class QSplitter; class QTabWidget; class QTimer; @@ -35,7 +31,6 @@ class GraphEditorWidget : public QWidget { public: explicit GraphEditorWidget(warppipe::Client *client, QWidget *parent = nullptr); - ~GraphEditorWidget() override; int nodeCount() const; int linkCount() const; @@ -69,14 +64,6 @@ private: void restoreViewState(); void savePreset(); void loadPreset(); - void wireVolumeWidget(QtNodes::NodeId nodeId); - void rebuildMixerStrips(); - void updateMeters(); - void rebuildNodeMeters(); - void rebuildRulesList(); - void showAddRuleDialog(const std::string &prefillApp = {}, - const std::string &prefillBin = {}, - const std::string &prefillRole = {}); struct PendingPasteLink { std::string outNodeName; @@ -92,7 +79,6 @@ private: QSplitter *m_splitter = nullptr; QTabWidget *m_sidebar = nullptr; QTimer *m_refreshTimer = nullptr; - QTimer *m_changeTimer = nullptr; QTimer *m_saveTimer = nullptr; QString m_layoutPath; QString m_presetDir; @@ -101,22 +87,4 @@ private: QJsonObject m_clipboardJson; std::vector m_pendingPasteLinks; QPointF m_lastContextMenuScenePos; - QWidget *m_mixerContainer = nullptr; - QScrollArea *m_mixerScroll = nullptr; - std::unordered_map m_mixerStrips; - - QTimer *m_meterTimer = nullptr; - AudioLevelMeter *m_masterMeterL = nullptr; - AudioLevelMeter *m_masterMeterR = nullptr; - QWidget *m_nodeMeterContainer = nullptr; - QScrollArea *m_nodeMeterScroll = nullptr; - struct NodeMeterRow { - QWidget *widget = nullptr; - AudioLevelMeter *meter = nullptr; - QLabel *label = nullptr; - }; - std::unordered_map m_nodeMeters; - - QWidget *m_rulesContainer = nullptr; - QScrollArea *m_rulesScroll = nullptr; }; diff --git a/gui/PresetManager.cpp b/gui/PresetManager.cpp index ba51577..c507d3d 100644 --- a/gui/PresetManager.cpp +++ b/gui/PresetManager.cpp @@ -84,28 +84,11 @@ bool PresetManager::savePreset(const QString &path, warppipe::Client *client, layoutArray.append(nodeLayout); } - QJsonArray volumesArray; - for (auto qtId : model->allNodeIds()) { - const WarpNodeData *data = model->warpNodeData(qtId); - if (!data) - continue; - auto vs = model->nodeVolumeState(qtId); - if (vs.volume != 1.0f || vs.mute) { - QJsonObject volObj; - volObj["name"] = QString::fromStdString(data->info.name); - volObj["volume"] = static_cast(vs.volume); - volObj["mute"] = vs.mute; - volumesArray.append(volObj); - } - } - QJsonObject root; root["version"] = 1; root["virtual_devices"] = devicesArray; root["routing"] = routingArray; root["layout"] = layoutArray; - if (!volumesArray.isEmpty()) - root["volumes"] = volumesArray; QFileInfo fi(path); QDir dir = fi.absoluteDir(); @@ -186,31 +169,9 @@ bool PresetManager::loadPreset(const QString &path, warppipe::Client *client, std::string inPort = route["in_port"].toString().toStdString(); client->CreateLinkByName(outNode, outPort, inNode, inPort, - warppipe::LinkOptions{.linger = true}); + warppipe::LinkOptions{}); } model->refreshFromClient(); - - if (root.contains("volumes")) { - QJsonArray volumesArray = root["volumes"].toArray(); - for (const auto &val : volumesArray) { - QJsonObject obj = val.toObject(); - std::string name = obj["name"].toString().toStdString(); - float volume = static_cast(obj["volume"].toDouble(1.0)); - bool mute = obj["mute"].toBool(false); - - for (auto qtId : model->allNodeIds()) { - const WarpNodeData *data = model->warpNodeData(qtId); - if (data && data->info.name == name) { - WarpGraphModel::NodeVolumeState vs; - vs.volume = volume; - vs.mute = mute; - model->setNodeVolumeState(qtId, vs); - break; - } - } - } - } - return true; } diff --git a/gui/VolumeWidgets.cpp b/gui/VolumeWidgets.cpp deleted file mode 100644 index ec80874..0000000 --- a/gui/VolumeWidgets.cpp +++ /dev/null @@ -1,105 +0,0 @@ -#include "VolumeWidgets.h" - -#include -#include -#include -#include - -ClickSlider::ClickSlider(Qt::Orientation orientation, QWidget *parent) - : QSlider(orientation, parent) {} - -void ClickSlider::mousePressEvent(QMouseEvent *event) { - QStyleOptionSlider opt; - initStyleOption(&opt); - QRect grooveRect = - style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderGroove, this); - QRect handleRect = - style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderHandle, this); - - int pos; - int span; - if (orientation() == Qt::Horizontal) { - pos = event->pos().x() - grooveRect.x() - handleRect.width() / 2; - span = grooveRect.width() - handleRect.width(); - } else { - pos = event->pos().y() - grooveRect.y() - handleRect.height() / 2; - span = grooveRect.height() - handleRect.height(); - } - - if (span > 0) { - int val; - if (orientation() == Qt::Horizontal) { - val = QStyle::sliderValueFromPosition(minimum(), maximum(), pos, span, opt.upsideDown); - } else { - val = QStyle::sliderValueFromPosition(minimum(), maximum(), pos, span, !opt.upsideDown); - } - setValue(val); - event->accept(); - } - - QSlider::mousePressEvent(event); -} - -static const char *kSliderStyle = - "QSlider::groove:horizontal {" - " background: #1a1a1e; border-radius: 3px; height: 6px; }" - "QSlider::handle:horizontal {" - " background: #ecf0f6; border-radius: 5px;" - " width: 10px; margin: -4px 0; }" - "QSlider::sub-page:horizontal {" - " background: #4caf50; border-radius: 3px; }"; - -static const char *kMuteBtnStyle = - "QToolButton {" - " background: #2e2e36; color: #ecf0f6; border: 1px solid #3a3a44;" - " border-radius: 4px; padding: 2px 6px; font-weight: bold; font-size: 11px; }" - "QToolButton:checked {" - " background: #b03030; color: #ecf0f6; border: 1px solid #d04040; }" - "QToolButton:hover { background: #3a3a44; }" - "QToolButton:checked:hover { background: #c04040; }"; - -NodeVolumeWidget::NodeVolumeWidget(QWidget *parent) : QWidget(parent) { - setAutoFillBackground(true); - QPalette pal = palette(); - pal.setColor(QPalette::Window, QColor(0x1a, 0x1a, 0x1e)); - setPalette(pal); - - m_slider = new ClickSlider(Qt::Horizontal, this); - m_slider->setRange(0, 100); - m_slider->setValue(100); - m_slider->setFixedWidth(100); - m_slider->setStyleSheet(QString::fromLatin1(kSliderStyle)); - - m_muteBtn = new QToolButton(this); - m_muteBtn->setText(QStringLiteral("M")); - m_muteBtn->setCheckable(true); - m_muteBtn->setFixedSize(22, 22); - m_muteBtn->setStyleSheet(QString::fromLatin1(kMuteBtnStyle)); - - auto *layout = new QHBoxLayout(this); - layout->setContentsMargins(4, 2, 4, 2); - layout->setSpacing(4); - layout->addWidget(m_slider); - layout->addWidget(m_muteBtn); - - connect(m_slider, &QSlider::valueChanged, this, - &NodeVolumeWidget::volumeChanged); - connect(m_slider, &QSlider::sliderReleased, this, - &NodeVolumeWidget::sliderReleased); - connect(m_muteBtn, &QToolButton::toggled, this, - &NodeVolumeWidget::muteToggled); -} - -int NodeVolumeWidget::volume() const { return m_slider->value(); } - -bool NodeVolumeWidget::isMuted() const { return m_muteBtn->isChecked(); } - -void NodeVolumeWidget::setVolume(int value) { - QSignalBlocker blocker(m_slider); - m_slider->setValue(value); -} - -void NodeVolumeWidget::setMuted(bool muted) { - QSignalBlocker blocker(m_muteBtn); - m_muteBtn->setChecked(muted); -} diff --git a/gui/VolumeWidgets.h b/gui/VolumeWidgets.h deleted file mode 100644 index fa61730..0000000 --- a/gui/VolumeWidgets.h +++ /dev/null @@ -1,35 +0,0 @@ -#pragma once - -#include -#include -#include - -class ClickSlider : public QSlider { - Q_OBJECT -public: - explicit ClickSlider(Qt::Orientation orientation, QWidget *parent = nullptr); - -protected: - void mousePressEvent(QMouseEvent *event) override; -}; - -class NodeVolumeWidget : public QWidget { - Q_OBJECT -public: - explicit NodeVolumeWidget(QWidget *parent = nullptr); - - int volume() const; - bool isMuted() const; - - void setVolume(int value); - void setMuted(bool muted); - -Q_SIGNALS: - void volumeChanged(int value); - void muteToggled(bool muted); - void sliderReleased(); - -private: - ClickSlider *m_slider = nullptr; - QToolButton *m_muteBtn = nullptr; -}; diff --git a/gui/WarpGraphModel.cpp b/gui/WarpGraphModel.cpp index a62a63a..9db3c33 100644 --- a/gui/WarpGraphModel.cpp +++ b/gui/WarpGraphModel.cpp @@ -1,5 +1,4 @@ #include "WarpGraphModel.h" -#include "VolumeWidgets.h" #include #include @@ -124,7 +123,7 @@ void WarpGraphModel::addConnection( warppipe::PortId outPortId = outIt->second.outputPorts[outIdx].id; warppipe::PortId inPortId = inIt->second.inputPorts[inIdx].id; - auto result = m_client->CreateLink(outPortId, inPortId, warppipe::LinkOptions{.linger = true}); + auto result = m_client->CreateLink(outPortId, inPortId, warppipe::LinkOptions{}); if (!result.ok()) { return; } @@ -179,12 +178,6 @@ QVariant WarpGraphModel::nodeData(QtNodes::NodeId nodeId, WarpNodeType type = classifyNode(data.info); return styleForNode(type, ghost); } - case QtNodes::NodeRole::Widget: { - auto wIt = m_volumeWidgets.find(nodeId); - if (wIt != m_volumeWidgets.end()) - return QVariant::fromValue(wIt->second); - return QVariant::fromValue(static_cast(nullptr)); - } default: return QVariant(); } @@ -297,12 +290,6 @@ bool WarpGraphModel::deleteNode(QtNodes::NodeId const nodeId) { m_nodes.erase(nodeId); m_positions.erase(nodeId); m_sizes.erase(nodeId); - m_volumeStates.erase(nodeId); - auto vwIt = m_volumeWidgets.find(nodeId); - if (vwIt != m_volumeWidgets.end()) { - delete vwIt->second; - m_volumeWidgets.erase(vwIt); - } Q_EMIT nodeDeleted(nodeId); return true; } @@ -470,10 +457,6 @@ void WarpGraphModel::refreshFromClient() { } } - auto *volumeWidget = new NodeVolumeWidget(); - m_volumeWidgets[qtId] = volumeWidget; - m_volumeStates[qtId] = {}; - Q_EMIT nodeCreated(qtId); } @@ -730,45 +713,6 @@ WarpGraphModel::classifyNode(const warppipe::NodeInfo &info) { return WarpNodeType::kUnknown; } -void WarpGraphModel::setNodeVolumeState(QtNodes::NodeId nodeId, - const NodeVolumeState &state) { - if (!nodeExists(nodeId)) - return; - - NodeVolumeState previous = m_volumeStates[nodeId]; - m_volumeStates[nodeId] = state; - - if (m_client) { - auto it = m_nodes.find(nodeId); - if (it != m_nodes.end() && it->second.info.id.value != 0) { -#ifdef WARPPIPE_TESTING - m_client->Test_SetNodeVolume(it->second.info.id, state.volume, state.mute); -#else - m_client->SetNodeVolume(it->second.info.id, state.volume, state.mute); -#endif - } - } - - auto wIt = m_volumeWidgets.find(nodeId); - if (wIt != m_volumeWidgets.end()) { - auto *w = qobject_cast(wIt->second); - if (w) { - w->setVolume(static_cast(state.volume * 100.0f)); - w->setMuted(state.mute); - } - } - - Q_EMIT nodeVolumeChanged(nodeId, previous, state); -} - -WarpGraphModel::NodeVolumeState -WarpGraphModel::nodeVolumeState(QtNodes::NodeId nodeId) const { - auto it = m_volumeStates.find(nodeId); - if (it != m_volumeStates.end()) - return it->second; - return {}; -} - void WarpGraphModel::saveLayout(const QString &path) const { ViewState vs{}; saveLayout(path, vs); @@ -994,10 +938,6 @@ bool WarpGraphModel::loadLayout(const QString &path) { ? m_positions.at(qtId) : QPointF(0, 0); - auto *volumeWidget = new NodeVolumeWidget(); - m_volumeWidgets[qtId] = volumeWidget; - m_volumeStates[qtId] = {}; - Q_EMIT nodeCreated(qtId); } } diff --git a/gui/WarpGraphModel.h b/gui/WarpGraphModel.h index e667eb5..f8658a5 100644 --- a/gui/WarpGraphModel.h +++ b/gui/WarpGraphModel.h @@ -69,19 +69,6 @@ public: uint32_t findPwNodeIdByName(const std::string &name) const; - struct NodeVolumeState { - float volume = 1.0f; - bool mute = false; - }; - - void setNodeVolumeState(QtNodes::NodeId nodeId, const NodeVolumeState &state); - NodeVolumeState nodeVolumeState(QtNodes::NodeId nodeId) const; - -Q_SIGNALS: - void nodeVolumeChanged(QtNodes::NodeId nodeId, NodeVolumeState previous, - NodeVolumeState current); - -public: struct ViewState { double scale; double centerX; @@ -138,7 +125,4 @@ private: std::unordered_map m_savedPositions; std::vector m_pendingGhostConnections; ViewState m_savedViewState{}; - - std::unordered_map m_volumeStates; - std::unordered_map m_volumeWidgets; }; diff --git a/include/warppipe/warppipe.hpp b/include/warppipe/warppipe.hpp index e04046e..c222736 100644 --- a/include/warppipe/warppipe.hpp +++ b/include/warppipe/warppipe.hpp @@ -1,7 +1,6 @@ #pragma once #include -#include #include #include #include @@ -138,16 +137,6 @@ struct RouteRule { std::string target_node; }; -struct VolumeState { - float volume = 1.0f; - bool mute = false; -}; - -struct MeterState { - float peak_left = 0.0f; - float peak_right = 0.0f; -}; - struct MetadataInfo { std::string default_sink_name; std::string default_source_name; @@ -177,14 +166,6 @@ class Client { const VirtualNodeOptions& options = VirtualNodeOptions{}); Status RemoveNode(NodeId node); - Status SetNodeVolume(NodeId node, float volume, bool mute); - Result GetNodeVolume(NodeId node) const; - - Status EnsureNodeMeter(NodeId node); - Status DisableNodeMeter(NodeId node); - Result NodeMeterPeak(NodeId node) const; - Result MeterPeak() const; - Result CreateLink(PortId output, PortId input, const LinkOptions& options); Result CreateLinkByName(std::string_view output_node, std::string_view output_port, @@ -204,9 +185,6 @@ class Client { Status SaveConfig(std::string_view path); Status LoadConfig(std::string_view path); - using ChangeCallback = std::function; - void SetChangeCallback(ChangeCallback callback); - #ifdef WARPPIPE_TESTING Status Test_InsertNode(const NodeInfo& node); Status Test_InsertPort(const PortInfo& port); @@ -215,10 +193,6 @@ class Client { Status Test_ForceDisconnect(); Status Test_TriggerPolicyCheck(); size_t Test_GetPendingAutoLinkCount() const; - Status Test_SetNodeVolume(NodeId node, float volume, bool mute); - Result Test_GetNodeVolume(NodeId node) const; - Status Test_SetNodeMeterPeak(NodeId node, float left, float right); - Status Test_SetMasterMeterPeak(float left, float right); #endif private: diff --git a/src/warppipe.cpp b/src/warppipe.cpp index b669cfa..83391a6 100644 --- a/src/warppipe.cpp +++ b/src/warppipe.cpp @@ -1,13 +1,10 @@ #include -#include #include -#include #include #include #include #include #include -#include #include #include @@ -17,8 +14,6 @@ #include #include -#include -#include #include #include @@ -125,8 +120,6 @@ struct LinkProxy { bool failed = false; std::string error; uint32_t id = SPA_ID_INVALID; - uint32_t output_port = 0; - uint32_t input_port = 0; }; void LinkProxyBound(void* data, uint32_t global_id) { @@ -246,52 +239,6 @@ static const pw_stream_events kStreamEvents = { .process = StreamProcess, }; -struct MeterStreamData { - uint32_t node_id = 0; - std::string target_name; - pw_stream* stream = nullptr; - spa_hook listener{}; - std::atomic peak_left{0.0f}; - std::atomic peak_right{0.0f}; -}; - -void NodeMeterProcess(void* data) { - auto* meter = static_cast(data); - if (!meter || !meter->stream) { - return; - } - pw_buffer* buf = pw_stream_dequeue_buffer(meter->stream); - if (!buf || !buf->buffer || buf->buffer->n_datas == 0) { - if (buf) { - pw_stream_queue_buffer(meter->stream, buf); - } - return; - } - spa_data* d = &buf->buffer->datas[0]; - if (!d->data || !d->chunk) { - pw_stream_queue_buffer(meter->stream, buf); - return; - } - const float* samples = static_cast(d->data); - uint32_t count = d->chunk->size / sizeof(float); - float left = 0.0f; - float right = 0.0f; - for (uint32_t i = 0; i + 1 < count; i += 2) { - float l = std::fabs(samples[i]); - float r = std::fabs(samples[i + 1]); - if (l > left) left = l; - if (r > right) right = r; - } - meter->peak_left.store(left, std::memory_order_relaxed); - meter->peak_right.store(right, std::memory_order_relaxed); - pw_stream_queue_buffer(meter->stream, buf); -} - -static const pw_stream_events kNodeMeterEvents = { - .version = PW_VERSION_STREAM_EVENTS, - .process = NodeMeterProcess, -}; - } // namespace Status Status::Ok() { @@ -330,45 +277,17 @@ struct Client::Impl { std::unordered_map> virtual_streams; std::unordered_map> link_proxies; - std::unordered_map volume_states; - - std::unordered_map meter_states; - std::unordered_set metered_nodes; - MeterState master_meter; - - std::unique_ptr master_meter_data; - std::unordered_map> live_meters; - uint32_t next_rule_id = 1; std::unordered_map route_rules; std::vector pending_auto_links; uint32_t policy_sync_seq = 0; bool policy_sync_pending = false; std::vector> auto_link_proxies; - std::vector> auto_link_claimed_pairs; - std::vector> saved_link_proxies; pw_proxy* metadata_proxy = nullptr; spa_hook metadata_listener{}; bool metadata_listener_attached = false; MetadataInfo defaults; - bool loading_config = false; - - struct SavedLink { - std::string out_node; - std::string out_port; - std::string in_node; - std::string in_port; - bool operator==(const SavedLink& o) const { - return out_node == o.out_node && out_port == o.out_port && - in_node == o.in_node && in_port == o.in_port; - } - }; - std::vector saved_links; - - std::mutex change_cb_mutex; - Client::ChangeCallback change_callback; - void NotifyChange(); Status ConnectLocked(); void DisconnectLocked(); @@ -380,16 +299,10 @@ struct Client::Impl { const VirtualNodeOptions& options); void CheckRulesForNode(const NodeInfo& node); - void EnforceRulesForLink(uint32_t link_id, uint32_t out_port, uint32_t in_port); void SchedulePolicySync(); void ProcessPendingAutoLinks(); void CreateAutoLinkAsync(uint32_t output_port, uint32_t input_port); - void ProcessSavedLinks(); - void CreateSavedLinkAsync(uint32_t output_port, uint32_t input_port); void AutoSave(); - void SetupMasterMeter(); - void TeardownMasterMeter(); - void TeardownAllLiveMeters(); static void RegistryGlobal(void* data, uint32_t id, @@ -415,68 +328,58 @@ void Client::Impl::RegistryGlobal(void* data, return; } - bool notify = false; + std::lock_guard lock(impl->cache_mutex); - { - std::lock_guard lock(impl->cache_mutex); - - if (IsNodeType(type)) { - NodeInfo info; - info.id = NodeId{id}; - info.name = LookupString(props, PW_KEY_NODE_NAME); - info.description = LookupString(props, PW_KEY_NODE_DESCRIPTION); - info.media_class = LookupString(props, PW_KEY_MEDIA_CLASS); - info.application_name = LookupString(props, PW_KEY_APP_NAME); - info.process_binary = LookupString(props, PW_KEY_APP_PROCESS_BINARY); - info.media_role = LookupString(props, PW_KEY_MEDIA_ROLE); - std::string virt_str = LookupString(props, PW_KEY_NODE_VIRTUAL); - info.is_virtual = (virt_str == "true"); - impl->nodes[id] = info; - impl->CheckRulesForNode(info); - notify = true; - } else if (IsPortType(type)) { - PortInfo info; - info.id = PortId{id}; - info.name = LookupString(props, PW_KEY_PORT_NAME); - info.is_input = false; - uint32_t node_id = 0; - if (ParseUint32(SafeLookup(props, PW_KEY_NODE_ID), &node_id)) { - info.node = NodeId{node_id}; - } - const char* direction = SafeLookup(props, PW_KEY_PORT_DIRECTION); - if (direction && spa_streq(direction, "in")) { - info.is_input = true; - } - impl->ports[id] = info; - if (!impl->pending_auto_links.empty() || !impl->saved_links.empty()) { - impl->SchedulePolicySync(); - } - notify = true; - } else if (IsLinkType(type)) { - Link info; - info.id = LinkId{id}; - uint32_t out_port = 0; - uint32_t in_port = 0; - if (ParseUint32(SafeLookup(props, PW_KEY_LINK_OUTPUT_PORT), &out_port)) { - info.output_port = PortId{out_port}; - } - if (ParseUint32(SafeLookup(props, PW_KEY_LINK_INPUT_PORT), &in_port)) { - info.input_port = PortId{in_port}; - } - impl->links[id] = std::move(info); - if (!impl->options.policy_only && out_port && in_port) { - impl->EnforceRulesForLink(id, out_port, in_port); - } - notify = true; - } - } - - if (notify) { - impl->NotifyChange(); + if (IsNodeType(type)) { + NodeInfo info; + info.id = NodeId{id}; + info.name = LookupString(props, PW_KEY_NODE_NAME); + info.description = LookupString(props, PW_KEY_NODE_DESCRIPTION); + info.media_class = LookupString(props, PW_KEY_MEDIA_CLASS); + info.application_name = LookupString(props, PW_KEY_APP_NAME); + info.process_binary = LookupString(props, PW_KEY_APP_PROCESS_BINARY); + info.media_role = LookupString(props, PW_KEY_MEDIA_ROLE); + std::string virt_str = LookupString(props, PW_KEY_NODE_VIRTUAL); + info.is_virtual = (virt_str == "true"); + impl->nodes[id] = info; + impl->CheckRulesForNode(info); return; } - std::lock_guard lock(impl->cache_mutex); + if (IsPortType(type)) { + PortInfo info; + info.id = PortId{id}; + info.name = LookupString(props, PW_KEY_PORT_NAME); + info.is_input = false; + uint32_t node_id = 0; + if (ParseUint32(SafeLookup(props, PW_KEY_NODE_ID), &node_id)) { + info.node = NodeId{node_id}; + } + const char* direction = SafeLookup(props, PW_KEY_PORT_DIRECTION); + if (direction && spa_streq(direction, "in")) { + info.is_input = true; + } + impl->ports[id] = info; + if (!impl->pending_auto_links.empty()) { + impl->SchedulePolicySync(); + } + return; + } + + if (IsLinkType(type)) { + Link info; + info.id = LinkId{id}; + uint32_t out_port = 0; + uint32_t in_port = 0; + if (ParseUint32(SafeLookup(props, PW_KEY_LINK_OUTPUT_PORT), &out_port)) { + info.output_port = PortId{out_port}; + } + if (ParseUint32(SafeLookup(props, PW_KEY_LINK_INPUT_PORT), &in_port)) { + info.input_port = PortId{in_port}; + } + impl->links[id] = std::move(info); + return; + } if (type && spa_streq(type, PW_TYPE_INTERFACE_Metadata)) { const char* meta_name = SafeLookup(props, "metadata.name"); @@ -505,49 +408,50 @@ void Client::Impl::RegistryGlobalRemove(void* data, uint32_t id) { return; } - { - std::lock_guard lock(impl->cache_mutex); - impl->virtual_streams.erase(id); - impl->link_proxies.erase(id); - auto node_it = impl->nodes.find(id); - if (node_it != impl->nodes.end()) { - impl->nodes.erase(node_it); - std::vector removed_ports; - for (auto it = impl->ports.begin(); it != impl->ports.end();) { - if (it->second.node.value == id) { - removed_ports.push_back(it->first); - it = impl->ports.erase(it); - } else { - ++it; - } + std::lock_guard lock(impl->cache_mutex); + impl->virtual_streams.erase(id); + impl->link_proxies.erase(id); + auto node_it = impl->nodes.find(id); + if (node_it != impl->nodes.end()) { + impl->nodes.erase(node_it); + std::vector removed_ports; + for (auto it = impl->ports.begin(); it != impl->ports.end();) { + if (it->second.node.value == id) { + removed_ports.push_back(it->first); + it = impl->ports.erase(it); + } else { + ++it; } - for (auto it = impl->links.begin(); it != impl->links.end();) { - bool remove_link = false; - for (uint32_t port_id : removed_ports) { - if (it->second.input_port.value == port_id || it->second.output_port.value == port_id) { - remove_link = true; - break; - } - } - if (remove_link) { - it = impl->links.erase(it); - } else { - ++it; - } - } - } else if (impl->ports.erase(id) > 0) { - for (auto it = impl->links.begin(); it != impl->links.end();) { - if (it->second.input_port.value == id || it->second.output_port.value == id) { - it = impl->links.erase(it); - } else { - ++it; - } - } - } else { - impl->links.erase(id); } + for (auto it = impl->links.begin(); it != impl->links.end();) { + bool remove_link = false; + for (uint32_t port_id : removed_ports) { + if (it->second.input_port.value == port_id || it->second.output_port.value == port_id) { + remove_link = true; + break; + } + } + if (remove_link) { + it = impl->links.erase(it); + } else { + ++it; + } + } + return; } - impl->NotifyChange(); + + if (impl->ports.erase(id) > 0) { + for (auto it = impl->links.begin(); it != impl->links.end();) { + if (it->second.input_port.value == id || it->second.output_port.value == id) { + it = impl->links.erase(it); + } else { + ++it; + } + } + return; + } + + impl->links.erase(id); } void Client::Impl::CoreDone(void* data, uint32_t, int seq) { @@ -562,10 +466,7 @@ void Client::Impl::CoreDone(void* data, uint32_t, int seq) { if (impl->policy_sync_pending && seq >= static_cast(impl->policy_sync_seq)) { impl->policy_sync_pending = false; - fprintf(stderr, "[WP] CoreDone policy sync seq=%d, pending_auto=%zu saved=%zu\n", - seq, impl->pending_auto_links.size(), impl->saved_links.size()); impl->ProcessPendingAutoLinks(); - impl->ProcessSavedLinks(); } } @@ -605,17 +506,9 @@ void Client::Impl::ClearCache() { ports.clear(); links.clear(); pending_auto_links.clear(); - auto_link_claimed_pairs.clear(); policy_sync_pending = false; } -void Client::Impl::NotifyChange() { - std::lock_guard lock(change_cb_mutex); - if (change_callback) { - change_callback(); - } -} - Status Client::Impl::EnsureConnected() { if (connected) { return Status::Ok(); @@ -833,14 +726,10 @@ Status Client::Impl::ConnectLocked() { if (!sync_status.ok()) { return sync_status; } - SetupMasterMeter(); return Status::Ok(); } void Client::Impl::DisconnectLocked() { - TeardownMasterMeter(); - TeardownAllLiveMeters(); - std::unordered_map> links; std::unordered_map> streams; { @@ -850,8 +739,8 @@ void Client::Impl::DisconnectLocked() { } for (auto& entry : links) { LinkProxy* link = entry.second.get(); - if (link) { - spa_hook_remove(&link->listener); + if (link && link->proxy) { + pw_proxy_destroy(link->proxy); link->proxy = nullptr; } } @@ -864,19 +753,12 @@ void Client::Impl::DisconnectLocked() { } } for (auto& entry : auto_link_proxies) { - if (entry) { - spa_hook_remove(&entry->listener); + if (entry && entry->proxy) { + pw_proxy_destroy(entry->proxy); entry->proxy = nullptr; } } auto_link_proxies.clear(); - for (auto& entry : saved_link_proxies) { - if (entry) { - spa_hook_remove(&entry->listener); - entry->proxy = nullptr; - } - } - saved_link_proxies.clear(); if (metadata_listener_attached) { spa_hook_remove(&metadata_listener); metadata_listener_attached = false; @@ -918,52 +800,6 @@ void Client::Impl::CheckRulesForNode(const NodeInfo& node) { } } -void Client::Impl::EnforceRulesForLink(uint32_t link_id, uint32_t out_port, - uint32_t in_port) { - auto port_it = ports.find(out_port); - if (port_it == ports.end()) return; - uint32_t src_node_id = port_it->second.node.value; - auto node_it = nodes.find(src_node_id); - if (node_it == nodes.end()) return; - - for (const auto& rule_entry : route_rules) { - if (!MatchesRule(node_it->second, rule_entry.second.match)) continue; - - uint32_t target_node_id = 0; - for (const auto& n : nodes) { - if (n.second.name == rule_entry.second.target_node) { - target_node_id = n.first; - break; - } - } - if (target_node_id == 0) return; - - auto in_port_it = ports.find(in_port); - if (in_port_it == ports.end()) return; - if (in_port_it->second.node.value == target_node_id) return; - - if (link_proxies.count(link_id)) return; - for (const auto& proxy : auto_link_proxies) { - if (proxy && proxy->output_port == out_port && - proxy->input_port == in_port) return; - } - for (const auto& proxy : saved_link_proxies) { - if (proxy && proxy->output_port == out_port && - proxy->input_port == in_port) return; - } - for (const auto& pair : auto_link_claimed_pairs) { - if (pair.first == out_port && pair.second == in_port) return; - } - - fprintf(stderr, "[WP] EnforceRule: destroying link %u (%u->%u), " - "rule says %s -> %s\n", link_id, out_port, in_port, - node_it->second.name.c_str(), - rule_entry.second.target_node.c_str()); - pw_registry_destroy(registry, link_id); - return; - } -} - void Client::Impl::SchedulePolicySync() { if (policy_sync_pending || !core) { return; @@ -987,7 +823,6 @@ void Client::Impl::ProcessPendingAutoLinks() { uint32_t input_port; }; std::vector links_to_create; - std::vector> batch_pairs; { std::lock_guard lock(cache_mutex); @@ -1001,8 +836,6 @@ void Client::Impl::ProcessPendingAutoLinks() { } } if (target_node_id == 0) { - fprintf(stderr, "[WP] AutoLink: target '%s' not found for src node %u\n", - it->target_node_name.c_str(), it->source_node_id); ++it; continue; } @@ -1025,9 +858,6 @@ void Client::Impl::ProcessPendingAutoLinks() { } if (src_ports.empty() || tgt_ports.empty()) { - fprintf(stderr, "[WP] AutoLink: src_ports=%zu tgt_ports=%zu for src=%u target=%u('%s')\n", - src_ports.size(), tgt_ports.size(), it->source_node_id, - target_node_id, it->target_node_name.c_str()); ++it; continue; } @@ -1048,15 +878,8 @@ void Client::Impl::ProcessPendingAutoLinks() { break; } } - auto_link_claimed_pairs.emplace_back(src_ports[i].id, tgt_ports[i].id); - batch_pairs.emplace_back(src_ports[i].id, tgt_ports[i].id); if (!exists) { links_to_create.push_back({src_ports[i].id, tgt_ports[i].id}); - fprintf(stderr, "[WP] AutoLink: will create %u->%u\n", - src_ports[i].id, tgt_ports[i].id); - } else { - fprintf(stderr, "[WP] AutoLink: already exists %u->%u (claimed)\n", - src_ports[i].id, tgt_ports[i].id); } } @@ -1064,52 +887,6 @@ void Client::Impl::ProcessPendingAutoLinks() { } } - if (batch_pairs.empty()) { - for (const auto& spec : links_to_create) { - CreateAutoLinkAsync(spec.output_port, spec.input_port); - } - return; - } - - std::unordered_map> auto_port_map; - for (const auto& pair : batch_pairs) { - auto_port_map[pair.first].push_back(pair.second); - } - - std::vector competing_ids; - { - std::lock_guard lock(cache_mutex); - for (const auto& link_entry : links) { - auto it = auto_port_map.find(link_entry.second.output_port.value); - if (it == auto_port_map.end()) continue; - uint32_t link_id = link_entry.first; - uint32_t in_port = link_entry.second.input_port.value; - bool is_ours = false; - for (uint32_t target_in : it->second) { - if (target_in == in_port) { is_ours = true; break; } - } - if (!is_ours) { - if (link_proxies.count(link_id)) is_ours = true; - } - if (!is_ours) { - uint32_t out_port = link_entry.second.output_port.value; - for (const auto& proxy : saved_link_proxies) { - if (proxy && proxy->output_port == out_port && - proxy->input_port == in_port) { is_ours = true; break; } - } - } - if (!is_ours) { - fprintf(stderr, "[WP] AutoLink competing: link %u (%u->%u) will destroy\n", - link_id, link_entry.second.output_port.value, in_port); - competing_ids.push_back(link_id); - } - } - } - - for (uint32_t id : competing_ids) { - pw_registry_destroy(registry, id); - } - for (const auto& spec : links_to_create) { CreateAutoLinkAsync(spec.output_port, spec.input_port); } @@ -1141,184 +918,14 @@ void Client::Impl::CreateAutoLinkAsync(uint32_t output_port, uint32_t input_port auto link_data = std::make_unique(); link_data->proxy = proxy; link_data->loop = thread_loop; - link_data->output_port = output_port; - link_data->input_port = input_port; pw_proxy_add_listener(proxy, &link_data->listener, &kLinkProxyEvents, link_data.get()); std::lock_guard lock(cache_mutex); auto_link_proxies.push_back(std::move(link_data)); } -void Client::Impl::ProcessSavedLinks() { - struct LinkSpec { - uint32_t output_port; - uint32_t input_port; - std::string label; - }; - std::vector to_create; - - { - std::lock_guard lock(cache_mutex); - for (auto it = saved_links.begin(); it != saved_links.end();) { - bool covered_by_rule = false; - for (const auto& node_entry : nodes) { - if (node_entry.second.name != it->out_node) continue; - for (const auto& rule_entry : route_rules) { - if (MatchesRule(node_entry.second, rule_entry.second.match) && - rule_entry.second.target_node == it->in_node) { - covered_by_rule = true; - break; - } - } - if (covered_by_rule) break; - } - if (covered_by_rule) { - fprintf(stderr, "[WP] SavedLink: covered_by_rule, skipping %s:%s -> %s:%s\n", - it->out_node.c_str(), it->out_port.c_str(), - it->in_node.c_str(), it->in_port.c_str()); - it = saved_links.erase(it); - continue; - } - uint32_t out_id = 0, in_id = 0; - for (const auto& port_entry : ports) { - const PortInfo& port = port_entry.second; - auto node_it = nodes.find(port.node.value); - if (node_it == nodes.end()) continue; - if (!port.is_input && node_it->second.name == it->out_node && - port.name == it->out_port) { - out_id = port_entry.first; - } - if (port.is_input && node_it->second.name == it->in_node && - port.name == it->in_port) { - in_id = port_entry.first; - } - if (out_id && in_id) break; - } - if (!out_id || !in_id) { - ++it; - continue; - } - bool exists = false; - for (const auto& link_entry : links) { - if (link_entry.second.output_port.value == out_id && - link_entry.second.input_port.value == in_id) { - exists = true; - break; - } - } - if (exists) { - fprintf(stderr, "[WP] SavedLink: already exists %s:%s -> %s:%s\n", - it->out_node.c_str(), it->out_port.c_str(), - it->in_node.c_str(), it->in_port.c_str()); - it = saved_links.erase(it); - continue; - } - std::string label = it->out_node + ":" + it->out_port + " -> " + - it->in_node + ":" + it->in_port; - to_create.push_back({out_id, in_id, std::move(label)}); - it = saved_links.erase(it); - } - } - - fprintf(stderr, "[WP] SavedLink: %zu links to create\n", to_create.size()); - if (to_create.empty()) return; - - std::unordered_map> saved_port_map; - for (const auto& spec : to_create) { - saved_port_map[spec.output_port].push_back(spec.input_port); - } - - std::vector competing_link_ids; - { - std::lock_guard lock(cache_mutex); - for (const auto& link_entry : links) { - auto it = saved_port_map.find(link_entry.second.output_port.value); - if (it == saved_port_map.end()) continue; - uint32_t link_id = link_entry.first; - uint32_t in_port = link_entry.second.input_port.value; - bool is_ours = false; - for (uint32_t saved_in : it->second) { - if (saved_in == in_port) { is_ours = true; break; } - } - if (!is_ours) { - if (link_proxies.count(link_id)) { - is_ours = true; - } - } - if (!is_ours) { - uint32_t out_port = link_entry.second.output_port.value; - for (const auto& proxy : auto_link_proxies) { - if (proxy && proxy->output_port == out_port && - proxy->input_port == in_port) { is_ours = true; break; } - } - } - if (!is_ours) { - uint32_t out_port = link_entry.second.output_port.value; - for (const auto& pair : auto_link_claimed_pairs) { - if (pair.first == out_port && pair.second == in_port) { - is_ours = true; break; - } - } - } - if (!is_ours) { - uint32_t out_port = link_entry.second.output_port.value; - for (const auto& proxy : saved_link_proxies) { - if (proxy && proxy->output_port == out_port && - proxy->input_port == in_port) { is_ours = true; break; } - } - } - if (!is_ours) { - fprintf(stderr, "[WP] Competing: link %u (%u->%u) has no owner, will destroy\n", - link_id, link_entry.second.output_port.value, in_port); - competing_link_ids.push_back(link_id); - } else { - fprintf(stderr, "[WP] Competing: link %u (%u->%u) is ours, keeping\n", - link_id, link_entry.second.output_port.value, in_port); - } - } - } - - for (uint32_t id : competing_link_ids) { - pw_registry_destroy(registry, id); - } - - for (const auto& spec : to_create) { - CreateSavedLinkAsync(spec.output_port, spec.input_port); - } -} - -void Client::Impl::CreateSavedLinkAsync(uint32_t output_port, - uint32_t input_port) { - if (!core) return; - - pw_properties* props = pw_properties_new(nullptr, nullptr); - if (!props) return; - pw_properties_setf(props, PW_KEY_LINK_OUTPUT_PORT, "%u", output_port); - pw_properties_setf(props, PW_KEY_LINK_INPUT_PORT, "%u", input_port); - pw_properties_set(props, PW_KEY_OBJECT_LINGER, "true"); - - pw_proxy* proxy = reinterpret_cast( - pw_core_create_object(core, "link-factory", - PW_TYPE_INTERFACE_Link, - PW_VERSION_LINK, - &props->dict, 0)); - pw_properties_free(props); - if (!proxy) return; - - auto link_data = std::make_unique(); - link_data->proxy = proxy; - link_data->loop = thread_loop; - link_data->output_port = output_port; - link_data->input_port = input_port; - pw_proxy_add_listener(proxy, &link_data->listener, &kLinkProxyEvents, - link_data.get()); - - std::lock_guard lock(cache_mutex); - saved_link_proxies.push_back(std::move(link_data)); -} - void Client::Impl::AutoSave() { - if (!options.config_path || options.config_path->empty() || loading_config) { + if (!options.config_path || options.config_path->empty()) { return; } nlohmann::json j; @@ -1359,83 +966,6 @@ void Client::Impl::AutoSave() { } j["route_rules"] = std::move(rules_array); - nlohmann::json links_array = nlohmann::json::array(); - { - std::lock_guard lock(cache_mutex); - std::vector live; - for (const auto& entry : link_proxies) { - if (!entry.second) { - continue; - } - auto link_it = links.find(entry.first); - if (link_it == links.end()) { - continue; - } - const Link& link = link_it->second; - auto out_port_it = ports.find(link.output_port.value); - auto in_port_it = ports.find(link.input_port.value); - if (out_port_it == ports.end() || in_port_it == ports.end()) { - continue; - } - auto out_node_it = nodes.find(out_port_it->second.node.value); - auto in_node_it = nodes.find(in_port_it->second.node.value); - if (out_node_it == nodes.end() || in_node_it == nodes.end()) { - continue; - } - SavedLink sl{out_node_it->second.name, out_port_it->second.name, - in_node_it->second.name, in_port_it->second.name}; - live.push_back(sl); - nlohmann::json link_obj; - link_obj["out_node"] = sl.out_node; - link_obj["out_port"] = sl.out_port; - link_obj["in_node"] = sl.in_node; - link_obj["in_port"] = sl.in_port; - links_array.push_back(std::move(link_obj)); - } - for (const auto& lp : saved_link_proxies) { - if (!lp || lp->id == SPA_ID_INVALID) continue; - auto link_it = links.find(lp->id); - if (link_it == links.end()) continue; - const Link& link = link_it->second; - auto out_port_it = ports.find(link.output_port.value); - auto in_port_it = ports.find(link.input_port.value); - if (out_port_it == ports.end() || in_port_it == ports.end()) continue; - auto out_node_it = nodes.find(out_port_it->second.node.value); - auto in_node_it = nodes.find(in_port_it->second.node.value); - if (out_node_it == nodes.end() || in_node_it == nodes.end()) continue; - SavedLink sl{out_node_it->second.name, out_port_it->second.name, - in_node_it->second.name, in_port_it->second.name}; - bool dup = false; - for (const auto& l : live) { - if (l == sl) { dup = true; break; } - } - if (!dup) { - live.push_back(sl); - nlohmann::json link_obj; - link_obj["out_node"] = sl.out_node; - link_obj["out_port"] = sl.out_port; - link_obj["in_node"] = sl.in_node; - link_obj["in_port"] = sl.in_port; - links_array.push_back(std::move(link_obj)); - } - } - for (const auto& sl : saved_links) { - bool already = false; - for (const auto& l : live) { - if (l == sl) { already = true; break; } - } - if (!already) { - nlohmann::json link_obj; - link_obj["out_node"] = sl.out_node; - link_obj["out_port"] = sl.out_port; - link_obj["in_node"] = sl.in_node; - link_obj["in_port"] = sl.in_port; - links_array.push_back(std::move(link_obj)); - } - } - } - j["links"] = std::move(links_array); - std::string tmp_path = *options.config_path + ".tmp"; std::ofstream file(tmp_path); if (!file.is_open()) { @@ -1448,74 +978,6 @@ void Client::Impl::AutoSave() { } } -void Client::Impl::SetupMasterMeter() { - if (!thread_loop || !core || master_meter_data) { - return; - } - auto meter = std::make_unique(); - pw_properties* props = pw_properties_new( - PW_KEY_MEDIA_TYPE, "Audio", - PW_KEY_MEDIA_CATEGORY, "Capture", - PW_KEY_MEDIA_CLASS, "Stream/Input/Audio", - PW_KEY_STREAM_CAPTURE_SINK, "true", - PW_KEY_STREAM_MONITOR, "true", - PW_KEY_NODE_NAME, "", - nullptr); - - meter->stream = pw_stream_new_simple( - pw_thread_loop_get_loop(thread_loop), - "warppipe-meter", props, &kNodeMeterEvents, meter.get()); - if (!meter->stream) { - return; - } - - uint8_t buffer[512]; - spa_pod_builder builder = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); - spa_audio_info_raw info{}; - info.format = SPA_AUDIO_FORMAT_F32; - info.rate = 48000; - info.channels = 2; - info.position[0] = SPA_AUDIO_CHANNEL_FL; - info.position[1] = SPA_AUDIO_CHANNEL_FR; - const spa_pod* params[1]; - params[0] = spa_format_audio_raw_build(&builder, SPA_PARAM_EnumFormat, &info); - - int res = pw_stream_connect( - meter->stream, PW_DIRECTION_INPUT, PW_ID_ANY, - static_cast( - PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS | PW_STREAM_FLAG_RT_PROCESS), - params, 1); - if (res != 0) { - pw_stream_destroy(meter->stream); - return; - } - master_meter_data = std::move(meter); -} - -void Client::Impl::TeardownMasterMeter() { - if (!master_meter_data) { - return; - } - if (master_meter_data->stream) { - pw_stream_destroy(master_meter_data->stream); - } - master_meter_data.reset(); -} - -void Client::Impl::TeardownAllLiveMeters() { - std::unordered_map> meters; - { - std::lock_guard lock(cache_mutex); - meters.swap(live_meters); - } - for (auto& entry : meters) { - if (entry.second && entry.second->stream) { - pw_stream_destroy(entry.second->stream); - entry.second->stream = nullptr; - } - } -} - int Client::Impl::MetadataProperty(void* data, uint32_t subject, const char* key, const char* type, const char* value) { @@ -1754,198 +1216,6 @@ Status Client::RemoveNode(NodeId node) { return Status::Ok(); } -Status Client::SetNodeVolume(NodeId node, float volume, bool mute) { - Status status = impl_->EnsureConnected(); - if (!status.ok()) { - return status; - } - if (node.value == 0) { - return Status::Error(StatusCode::kInvalidArgument, "invalid node id"); - } - - volume = std::clamp(volume, 0.0f, 1.5f); - - pw_thread_loop_lock(impl_->thread_loop); - - { - std::lock_guard lock(impl_->cache_mutex); - if (impl_->nodes.find(node.value) == impl_->nodes.end()) { - pw_thread_loop_unlock(impl_->thread_loop); - return Status::Error(StatusCode::kNotFound, "node not found"); - } - } - - auto* proxy = static_cast( - pw_registry_bind(impl_->registry, node.value, - PW_TYPE_INTERFACE_Node, PW_VERSION_NODE, 0)); - if (!proxy) { - pw_thread_loop_unlock(impl_->thread_loop); - return Status::Error(StatusCode::kInternal, "failed to bind node proxy"); - } - - 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(proxy, SPA_PARAM_Props, 0, param); - pw_proxy_destroy(reinterpret_cast(proxy)); - - { - std::lock_guard lock(impl_->cache_mutex); - impl_->volume_states[node.value] = VolumeState{volume, mute}; - } - - pw_thread_loop_unlock(impl_->thread_loop); - return Status::Ok(); -} - -Result Client::GetNodeVolume(NodeId node) const { - std::lock_guard lock(impl_->cache_mutex); - auto it = impl_->volume_states.find(node.value); - if (it == impl_->volume_states.end()) { - return {Status::Ok(), VolumeState{}}; - } - return {Status::Ok(), it->second}; -} - -Status Client::EnsureNodeMeter(NodeId node) { - if (node.value == 0) { - return Status::Error(StatusCode::kInvalidArgument, "invalid node id"); - } - - std::string target_name; - bool capture_sink = false; - { - std::lock_guard lock(impl_->cache_mutex); - auto node_it = impl_->nodes.find(node.value); - if (node_it == impl_->nodes.end()) { - return Status::Error(StatusCode::kNotFound, "node not found"); - } - impl_->metered_nodes.insert(node.value); - if (impl_->meter_states.find(node.value) == impl_->meter_states.end()) { - impl_->meter_states[node.value] = MeterState{}; - } - if (impl_->live_meters.find(node.value) != impl_->live_meters.end()) { - return Status::Ok(); - } - target_name = node_it->second.name; - const auto& mc = node_it->second.media_class; - capture_sink = (mc.find("Sink") != std::string::npos || - mc.find("Duplex") != std::string::npos); - } - - if (!impl_->thread_loop || !impl_->core) { - return Status::Ok(); - } - - pw_thread_loop_lock(impl_->thread_loop); - - auto meter = std::make_unique(); - meter->node_id = node.value; - meter->target_name = target_name; - - pw_properties* props = pw_properties_new( - PW_KEY_MEDIA_TYPE, "Audio", - PW_KEY_MEDIA_CATEGORY, "Capture", - PW_KEY_MEDIA_CLASS, "Stream/Input/Audio", - PW_KEY_TARGET_OBJECT, target_name.c_str(), - PW_KEY_STREAM_MONITOR, "true", - PW_KEY_NODE_NAME, "", - nullptr); - if (capture_sink) { - pw_properties_set(props, PW_KEY_STREAM_CAPTURE_SINK, "true"); - } - - meter->stream = pw_stream_new_simple( - pw_thread_loop_get_loop(impl_->thread_loop), - "warppipe-node-meter", props, &kNodeMeterEvents, meter.get()); - if (!meter->stream) { - pw_thread_loop_unlock(impl_->thread_loop); - return Status::Ok(); - } - - uint8_t buffer[512]; - spa_pod_builder builder = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); - spa_audio_info_raw info{}; - info.format = SPA_AUDIO_FORMAT_F32; - info.rate = 48000; - info.channels = 2; - info.position[0] = SPA_AUDIO_CHANNEL_FL; - info.position[1] = SPA_AUDIO_CHANNEL_FR; - const spa_pod* params[1]; - params[0] = spa_format_audio_raw_build(&builder, SPA_PARAM_EnumFormat, &info); - - int res = pw_stream_connect( - meter->stream, PW_DIRECTION_INPUT, PW_ID_ANY, - static_cast( - PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS | PW_STREAM_FLAG_RT_PROCESS), - params, 1); - if (res != 0) { - pw_stream_destroy(meter->stream); - pw_thread_loop_unlock(impl_->thread_loop); - return Status::Ok(); - } - - { - std::lock_guard lock(impl_->cache_mutex); - impl_->live_meters[node.value] = std::move(meter); - } - pw_thread_loop_unlock(impl_->thread_loop); - return Status::Ok(); -} - -Status Client::DisableNodeMeter(NodeId node) { - std::unique_ptr meter; - { - std::lock_guard lock(impl_->cache_mutex); - impl_->metered_nodes.erase(node.value); - impl_->meter_states.erase(node.value); - auto it = impl_->live_meters.find(node.value); - if (it != impl_->live_meters.end()) { - meter = std::move(it->second); - impl_->live_meters.erase(it); - } - } - if (meter && meter->stream && impl_->thread_loop) { - pw_thread_loop_lock(impl_->thread_loop); - pw_stream_destroy(meter->stream); - meter->stream = nullptr; - pw_thread_loop_unlock(impl_->thread_loop); - } - return Status::Ok(); -} - -Result Client::NodeMeterPeak(NodeId node) const { - std::lock_guard lock(impl_->cache_mutex); - auto live_it = impl_->live_meters.find(node.value); - if (live_it != impl_->live_meters.end() && live_it->second) { - MeterState state; - state.peak_left = live_it->second->peak_left.load(std::memory_order_relaxed); - state.peak_right = live_it->second->peak_right.load(std::memory_order_relaxed); - return {Status::Ok(), state}; - } - auto it = impl_->meter_states.find(node.value); - if (it == impl_->meter_states.end()) { - return {Status::Error(StatusCode::kNotFound, "node not metered"), {}}; - } - return {Status::Ok(), it->second}; -} - -Result Client::MeterPeak() const { - std::lock_guard lock(impl_->cache_mutex); - if (impl_->master_meter_data) { - MeterState state; - state.peak_left = impl_->master_meter_data->peak_left.load(std::memory_order_relaxed); - state.peak_right = impl_->master_meter_data->peak_right.load(std::memory_order_relaxed); - return {Status::Ok(), state}; - } - return {Status::Ok(), impl_->master_meter}; -} - Result Client::CreateLink(PortId output, PortId input, const LinkOptions& options) { Status status = impl_->EnsureConnected(); if (!status.ok()) { @@ -2017,8 +1287,6 @@ Result Client::CreateLink(PortId output, PortId input, const LinkOptions& auto link_proxy = std::make_unique(); link_proxy->proxy = proxy; link_proxy->loop = impl_->thread_loop; - link_proxy->output_port = output.value; - link_proxy->input_port = input.value; pw_proxy_add_listener(proxy, &link_proxy->listener, &kLinkProxyEvents, link_proxy.get()); int wait_attempts = 0; @@ -2053,7 +1321,6 @@ Result Client::CreateLink(PortId output, PortId input, const LinkOptions& } pw_thread_loop_unlock(impl_->thread_loop); - impl_->AutoSave(); return {Status::Ok(), link}; } @@ -2113,22 +1380,8 @@ Status Client::RemoveLink(LinkId link) { pw_thread_loop_lock(impl_->thread_loop); bool removed = false; - Impl::SavedLink removed_link; { std::lock_guard lock(impl_->cache_mutex); - auto link_it = impl_->links.find(link.value); - if (link_it != impl_->links.end()) { - auto op = impl_->ports.find(link_it->second.output_port.value); - auto ip = impl_->ports.find(link_it->second.input_port.value); - if (op != impl_->ports.end() && ip != impl_->ports.end()) { - auto on = impl_->nodes.find(op->second.node.value); - auto in_ = impl_->nodes.find(ip->second.node.value); - if (on != impl_->nodes.end() && in_ != impl_->nodes.end()) { - removed_link = {on->second.name, op->second.name, - in_->second.name, ip->second.name}; - } - } - } auto it = impl_->link_proxies.find(link.value); if (it != impl_->link_proxies.end()) { if (it->second && it->second->proxy) { @@ -2150,14 +1403,6 @@ Status Client::RemoveLink(LinkId link) { } pw_thread_loop_unlock(impl_->thread_loop); - if (removed) { - if (!removed_link.out_node.empty()) { - std::lock_guard lock(impl_->cache_mutex); - auto& sl = impl_->saved_links; - sl.erase(std::remove(sl.begin(), sl.end(), removed_link), sl.end()); - } - impl_->AutoSave(); - } return removed ? Status::Ok() : Status::Error(StatusCode::kNotFound, "link not found"); } @@ -2217,7 +1462,6 @@ Status Client::RemoveRouteRule(RuleId id) { ++pending_it; } } - impl_->auto_link_claimed_pairs.clear(); } impl_->AutoSave(); @@ -2315,85 +1559,8 @@ Status Client::SaveConfig(std::string_view path) { } } - nlohmann::json links_array = nlohmann::json::array(); - { - std::lock_guard lock(impl_->cache_mutex); - std::vector live; - for (const auto& entry : impl_->link_proxies) { - if (!entry.second) { - continue; - } - auto link_it = impl_->links.find(entry.first); - if (link_it == impl_->links.end()) { - continue; - } - const Link& link = link_it->second; - auto out_port_it = impl_->ports.find(link.output_port.value); - auto in_port_it = impl_->ports.find(link.input_port.value); - if (out_port_it == impl_->ports.end() || in_port_it == impl_->ports.end()) { - continue; - } - auto out_node_it = impl_->nodes.find(out_port_it->second.node.value); - auto in_node_it = impl_->nodes.find(in_port_it->second.node.value); - if (out_node_it == impl_->nodes.end() || in_node_it == impl_->nodes.end()) { - continue; - } - Impl::SavedLink sl{out_node_it->second.name, out_port_it->second.name, - in_node_it->second.name, in_port_it->second.name}; - live.push_back(sl); - nlohmann::json link_obj; - link_obj["out_node"] = sl.out_node; - link_obj["out_port"] = sl.out_port; - link_obj["in_node"] = sl.in_node; - link_obj["in_port"] = sl.in_port; - links_array.push_back(std::move(link_obj)); - } - for (const auto& lp : impl_->saved_link_proxies) { - if (!lp || lp->id == SPA_ID_INVALID) continue; - auto link_it = impl_->links.find(lp->id); - if (link_it == impl_->links.end()) continue; - const Link& link = link_it->second; - auto out_port_it = impl_->ports.find(link.output_port.value); - auto in_port_it = impl_->ports.find(link.input_port.value); - if (out_port_it == impl_->ports.end() || in_port_it == impl_->ports.end()) continue; - auto out_node_it = impl_->nodes.find(out_port_it->second.node.value); - auto in_node_it = impl_->nodes.find(in_port_it->second.node.value); - if (out_node_it == impl_->nodes.end() || in_node_it == impl_->nodes.end()) continue; - Impl::SavedLink sl{out_node_it->second.name, out_port_it->second.name, - in_node_it->second.name, in_port_it->second.name}; - bool dup = false; - for (const auto& l : live) { - if (l == sl) { dup = true; break; } - } - if (!dup) { - live.push_back(sl); - nlohmann::json link_obj; - link_obj["out_node"] = sl.out_node; - link_obj["out_port"] = sl.out_port; - link_obj["in_node"] = sl.in_node; - link_obj["in_port"] = sl.in_port; - links_array.push_back(std::move(link_obj)); - } - } - for (const auto& sl : impl_->saved_links) { - bool already = false; - for (const auto& l : live) { - if (l == sl) { already = true; break; } - } - if (!already) { - nlohmann::json link_obj; - link_obj["out_node"] = sl.out_node; - link_obj["out_port"] = sl.out_port; - link_obj["in_node"] = sl.in_node; - link_obj["in_port"] = sl.in_port; - links_array.push_back(std::move(link_obj)); - } - } - } - j["virtual_nodes"] = std::move(nodes_array); j["route_rules"] = std::move(rules_array); - j["links"] = std::move(links_array); std::string tmp_path = std::string(path) + ".tmp"; std::ofstream file(tmp_path); @@ -2411,11 +1578,6 @@ Status Client::SaveConfig(std::string_view path) { return Status::Ok(); } -void Client::SetChangeCallback(ChangeCallback callback) { - std::lock_guard lock(impl_->change_cb_mutex); - impl_->change_callback = std::move(callback); -} - Status Client::LoadConfig(std::string_view path) { if (path.empty()) { return Status::Error(StatusCode::kInvalidArgument, "path is empty"); @@ -2439,8 +1601,6 @@ Status Client::LoadConfig(std::string_view path) { return Status::Error(StatusCode::kInvalidArgument, "config missing version"); } - impl_->loading_config = true; - if (j.contains("route_rules") && j["route_rules"].is_array()) { for (const auto& rule_obj : j["route_rules"]) { try { @@ -2494,38 +1654,6 @@ Status Client::LoadConfig(std::string_view path) { } } - if (j.contains("links") && j["links"].is_array()) { - { - std::lock_guard lock(impl_->cache_mutex); - for (const auto& link_obj : j["links"]) { - try { - std::string out_node = link_obj.value("out_node", ""); - std::string out_port = link_obj.value("out_port", ""); - std::string in_node = link_obj.value("in_node", ""); - std::string in_port = link_obj.value("in_port", ""); - if (out_node.empty() || out_port.empty() || - in_node.empty() || in_port.empty()) { - continue; - } - fprintf(stderr, "[WP] Config: loaded saved link %s:%s -> %s:%s\n", - out_node.c_str(), out_port.c_str(), - in_node.c_str(), in_port.c_str()); - impl_->saved_links.push_back({out_node, out_port, in_node, in_port}); - } catch (...) { - continue; - } - } - } - - if (conn_status.ok()) { - pw_thread_loop_lock(impl_->thread_loop); - impl_->SchedulePolicySync(); - pw_thread_loop_unlock(impl_->thread_loop); - } - } - - impl_->loading_config = false; - impl_->AutoSave(); return Status::Ok(); } @@ -2534,12 +1662,9 @@ Status Client::Test_InsertNode(const NodeInfo& node) { if (!impl_) { return Status::Error(StatusCode::kInternal, "client not initialized"); } - { - std::lock_guard lock(impl_->cache_mutex); - impl_->nodes[node.id.value] = node; - impl_->CheckRulesForNode(node); - } - impl_->NotifyChange(); + std::lock_guard lock(impl_->cache_mutex); + impl_->nodes[node.id.value] = node; + impl_->CheckRulesForNode(node); return Status::Ok(); } @@ -2547,11 +1672,8 @@ Status Client::Test_InsertPort(const PortInfo& port) { if (!impl_) { return Status::Error(StatusCode::kInternal, "client not initialized"); } - { - std::lock_guard lock(impl_->cache_mutex); - impl_->ports[port.id.value] = port; - } - impl_->NotifyChange(); + std::lock_guard lock(impl_->cache_mutex); + impl_->ports[port.id.value] = port; return Status::Ok(); } @@ -2559,11 +1681,8 @@ Status Client::Test_InsertLink(const Link& link) { if (!impl_) { return Status::Error(StatusCode::kInternal, "client not initialized"); } - { - std::lock_guard lock(impl_->cache_mutex); - impl_->links[link.id.value] = link; - } - impl_->NotifyChange(); + std::lock_guard lock(impl_->cache_mutex); + impl_->links[link.id.value] = link; return Status::Ok(); } @@ -2600,62 +1719,6 @@ size_t Client::Test_GetPendingAutoLinkCount() const { std::lock_guard lock(impl_->cache_mutex); return impl_->pending_auto_links.size(); } - -Status Client::Test_SetNodeVolume(NodeId node, float volume, bool mute) { - if (!impl_) { - return Status::Error(StatusCode::kUnavailable, "no impl"); - } - std::lock_guard lock(impl_->cache_mutex); - if (impl_->nodes.find(node.value) == impl_->nodes.end()) { - return Status::Error(StatusCode::kNotFound, "node not found"); - } - impl_->volume_states[node.value] = VolumeState{std::clamp(volume, 0.0f, 1.5f), mute}; - return Status::Ok(); -} - -Result Client::Test_GetNodeVolume(NodeId node) const { - if (!impl_) { - return {Status::Error(StatusCode::kUnavailable, "no impl"), {}}; - } - std::lock_guard lock(impl_->cache_mutex); - auto it = impl_->volume_states.find(node.value); - if (it == impl_->volume_states.end()) { - return {Status::Ok(), VolumeState{}}; - } - return {Status::Ok(), it->second}; -} - -Status Client::Test_SetNodeMeterPeak(NodeId node, float left, float right) { - if (!impl_) { - return Status::Error(StatusCode::kUnavailable, "no impl"); - } - std::lock_guard lock(impl_->cache_mutex); - float cl = std::clamp(left, 0.0f, 1.0f); - float cr = std::clamp(right, 0.0f, 1.0f); - impl_->meter_states[node.value] = MeterState{cl, cr}; - impl_->metered_nodes.insert(node.value); - auto it = impl_->live_meters.find(node.value); - if (it != impl_->live_meters.end() && it->second) { - it->second->peak_left.store(cl, std::memory_order_relaxed); - it->second->peak_right.store(cr, std::memory_order_relaxed); - } - return Status::Ok(); -} - -Status Client::Test_SetMasterMeterPeak(float left, float right) { - if (!impl_) { - return Status::Error(StatusCode::kUnavailable, "no impl"); - } - std::lock_guard lock(impl_->cache_mutex); - float cl = std::clamp(left, 0.0f, 1.0f); - float cr = std::clamp(right, 0.0f, 1.0f); - impl_->master_meter = MeterState{cl, cr}; - if (impl_->master_meter_data) { - impl_->master_meter_data->peak_left.store(cl, std::memory_order_relaxed); - impl_->master_meter_data->peak_right.store(cr, std::memory_order_relaxed); - } - return Status::Ok(); -} #endif } // namespace warppipe diff --git a/tests/gui/warppipe_gui_tests.cpp b/tests/gui/warppipe_gui_tests.cpp index d5e2c6d..94473ae 100644 --- a/tests/gui/warppipe_gui_tests.cpp +++ b/tests/gui/warppipe_gui_tests.cpp @@ -1,9 +1,7 @@ #include -#include "../../gui/AudioLevelMeter.h" #include "../../gui/GraphEditorWidget.h" #include "../../gui/PresetManager.h" -#include "../../gui/VolumeWidgets.h" #include "../../gui/WarpGraphModel.h" #include @@ -13,7 +11,6 @@ #include #include #include -#include #include #include @@ -966,361 +963,3 @@ TEST_CASE("splitter sizes persist in layout JSON") { QFile::remove(path); } - -TEST_CASE("model volume state defaults to 1.0 and unmuted") { - auto tc = TestClient::Create(); - if (!tc.available()) { SUCCEED("PipeWire unavailable"); return; } - ensureApp(); - - REQUIRE(tc.client->Test_InsertNode( - MakeNode(100600, "vol-default", "Audio/Sink")).ok()); - - WarpGraphModel model(tc.client.get()); - model.refreshFromClient(); - - auto qtId = model.qtNodeIdForPw(100600); - REQUIRE(qtId != 0); - - auto state = model.nodeVolumeState(qtId); - REQUIRE(state.volume == Catch::Approx(1.0f)); - REQUIRE_FALSE(state.mute); -} - -TEST_CASE("setNodeVolumeState updates model and calls test helper") { - auto tc = TestClient::Create(); - if (!tc.available()) { SUCCEED("PipeWire unavailable"); return; } - ensureApp(); - - REQUIRE(tc.client->Test_InsertNode( - MakeNode(100610, "vol-set", "Audio/Sink")).ok()); - - WarpGraphModel model(tc.client.get()); - model.refreshFromClient(); - - auto qtId = model.qtNodeIdForPw(100610); - REQUIRE(qtId != 0); - - WarpGraphModel::NodeVolumeState ns; - ns.volume = 0.5f; - ns.mute = true; - model.setNodeVolumeState(qtId, ns); - - auto state = model.nodeVolumeState(qtId); - REQUIRE(state.volume == Catch::Approx(0.5f)); - REQUIRE(state.mute); - - auto apiState = tc.client->Test_GetNodeVolume(warppipe::NodeId{100610}); - REQUIRE(apiState.ok()); - REQUIRE(apiState.value.volume == Catch::Approx(0.5f)); - REQUIRE(apiState.value.mute); -} - -TEST_CASE("nodeVolumeChanged signal emitted on state change") { - auto tc = TestClient::Create(); - if (!tc.available()) { SUCCEED("PipeWire unavailable"); return; } - ensureApp(); - - REQUIRE(tc.client->Test_InsertNode( - MakeNode(100620, "vol-signal", "Audio/Sink")).ok()); - - WarpGraphModel model(tc.client.get()); - model.refreshFromClient(); - - auto qtId = model.qtNodeIdForPw(100620); - REQUIRE(qtId != 0); - - bool signalFired = false; - QObject::connect(&model, &WarpGraphModel::nodeVolumeChanged, - [&](QtNodes::NodeId id, WarpGraphModel::NodeVolumeState prev, - WarpGraphModel::NodeVolumeState cur) { - if (id == qtId) { - signalFired = true; - REQUIRE(prev.volume == Catch::Approx(1.0f)); - REQUIRE(cur.volume == Catch::Approx(0.3f)); - REQUIRE(cur.mute); - } - }); - - WarpGraphModel::NodeVolumeState ns; - ns.volume = 0.3f; - ns.mute = true; - model.setNodeVolumeState(qtId, ns); - REQUIRE(signalFired); -} - -TEST_CASE("volume widget created for new nodes") { - auto tc = TestClient::Create(); - if (!tc.available()) { SUCCEED("PipeWire unavailable"); return; } - ensureApp(); - - REQUIRE(tc.client->Test_InsertNode( - MakeNode(100630, "vol-widget", "Audio/Sink")).ok()); - - WarpGraphModel model(tc.client.get()); - model.refreshFromClient(); - - auto qtId = model.qtNodeIdForPw(100630); - REQUIRE(qtId != 0); - - auto widget = model.nodeData(qtId, QtNodes::NodeRole::Widget); - REQUIRE(widget.isValid()); - auto *w = widget.value(); - REQUIRE(w != nullptr); - auto *vol = qobject_cast(w); - REQUIRE(vol != nullptr); - REQUIRE(vol->volume() == 100); - REQUIRE_FALSE(vol->isMuted()); -} - -TEST_CASE("setNodeVolumeState syncs inline widget") { - auto tc = TestClient::Create(); - if (!tc.available()) { SUCCEED("PipeWire unavailable"); return; } - ensureApp(); - - REQUIRE(tc.client->Test_InsertNode( - MakeNode(100640, "vol-sync", "Audio/Sink")).ok()); - - WarpGraphModel model(tc.client.get()); - model.refreshFromClient(); - - auto qtId = model.qtNodeIdForPw(100640); - auto *w = model.nodeData(qtId, QtNodes::NodeRole::Widget).value(); - auto *vol = qobject_cast(w); - REQUIRE(vol != nullptr); - - WarpGraphModel::NodeVolumeState ns; - ns.volume = 0.7f; - ns.mute = true; - model.setNodeVolumeState(qtId, ns); - - REQUIRE(vol->volume() == 70); - REQUIRE(vol->isMuted()); -} - -TEST_CASE("preset saves and loads volume state") { - auto tc = TestClient::Create(); - if (!tc.available()) { SUCCEED("PipeWire unavailable"); return; } - ensureApp(); - - REQUIRE(tc.client->Test_InsertNode( - MakeNode(100650, "vol-preset", "Audio/Sink", {}, {}, true)).ok()); - REQUIRE(tc.client->Test_InsertPort( - MakePort(100651, 100650, "FL", true)).ok()); - - WarpGraphModel model(tc.client.get()); - model.refreshFromClient(); - - auto qtId = model.qtNodeIdForPw(100650); - WarpGraphModel::NodeVolumeState ns; - ns.volume = 0.6f; - ns.mute = true; - model.setNodeVolumeState(qtId, ns); - - QString path = QStandardPaths::writableLocation( - QStandardPaths::TempLocation) + - "/warppipe_test_vol_preset.json"; - REQUIRE(PresetManager::savePreset(path, tc.client.get(), &model)); - - QFile file(path); - REQUIRE(file.open(QIODevice::ReadOnly)); - QJsonDocument doc = QJsonDocument::fromJson(file.readAll()); - file.close(); - QJsonObject root = doc.object(); - REQUIRE(root.contains("volumes")); - QJsonArray volArr = root["volumes"].toArray(); - bool found = false; - for (const auto &val : volArr) { - QJsonObject obj = val.toObject(); - if (obj["name"].toString() == "vol-preset") { - found = true; - REQUIRE(obj["volume"].toDouble() == Catch::Approx(0.6)); - REQUIRE(obj["mute"].toBool()); - } - } - REQUIRE(found); - - WarpGraphModel model2(tc.client.get()); - model2.refreshFromClient(); - auto qtId2 = model2.qtNodeIdForPw(100650); - auto stateBefore = model2.nodeVolumeState(qtId2); - REQUIRE(stateBefore.volume == Catch::Approx(1.0f)); - - REQUIRE(PresetManager::loadPreset(path, tc.client.get(), &model2)); - auto stateAfter = model2.nodeVolumeState(qtId2); - REQUIRE(stateAfter.volume == Catch::Approx(0.6f)); - REQUIRE(stateAfter.mute); - - QFile::remove(path); -} - -TEST_CASE("AudioLevelMeter setLevel clamps to 0-1") { - ensureApp(); - AudioLevelMeter meter; - meter.setLevel(0.5f); - REQUIRE(meter.level() == Catch::Approx(0.5f)); - meter.setLevel(-0.5f); - REQUIRE(meter.level() == Catch::Approx(0.0f)); - meter.setLevel(1.5f); - REQUIRE(meter.level() == Catch::Approx(1.0f)); -} - -TEST_CASE("AudioLevelMeter peak hold tracks maximum") { - ensureApp(); - AudioLevelMeter meter; - meter.setLevel(0.8f); - REQUIRE(meter.peakHold() == Catch::Approx(0.8f)); - meter.setLevel(0.3f); - REQUIRE(meter.peakHold() == Catch::Approx(0.8f)); - meter.setLevel(0.9f); - REQUIRE(meter.peakHold() == Catch::Approx(0.9f)); -} - -TEST_CASE("AudioLevelMeter peak decays after hold period") { - ensureApp(); - AudioLevelMeter meter; - meter.setLevel(0.5f); - REQUIRE(meter.peakHold() == Catch::Approx(0.5f)); - for (int i = 0; i < 7; ++i) - meter.setLevel(0.0f); - REQUIRE(meter.peakHold() < 0.5f); - REQUIRE(meter.peakHold() > 0.0f); -} - -TEST_CASE("AudioLevelMeter resetPeakHold clears peak") { - ensureApp(); - AudioLevelMeter meter; - meter.setLevel(0.7f); - REQUIRE(meter.peakHold() == Catch::Approx(0.7f)); - meter.resetPeakHold(); - REQUIRE(meter.peakHold() == Catch::Approx(0.0f)); -} - -TEST_CASE("GraphEditorWidget has METERS tab") { - auto tc = TestClient::Create(); - if (!tc.available()) { SUCCEED("PipeWire unavailable"); return; } - ensureApp(); - - GraphEditorWidget widget(tc.client.get()); - auto *sidebar = widget.findChild(); - REQUIRE(sidebar != nullptr); - bool found = false; - for (int i = 0; i < sidebar->count(); ++i) { - if (sidebar->tabText(i) == "METERS") { - found = true; - break; - } - } - REQUIRE(found); -} - -TEST_CASE("node meter rows created for injected nodes") { - auto tc = TestClient::Create(); - if (!tc.available()) { SUCCEED("PipeWire unavailable"); return; } - ensureApp(); - - REQUIRE(tc.client->Test_InsertNode( - MakeNode(100700, "meter-node", "Audio/Sink")).ok()); - REQUIRE(tc.client->Test_InsertPort( - MakePort(100701, 100700, "FL", true)).ok()); - - GraphEditorWidget widget(tc.client.get()); - auto meters = widget.findChildren(); - REQUIRE(meters.size() >= 3); -} - -TEST_CASE("volume state cleaned up on node deletion") { - auto tc = TestClient::Create(); - if (!tc.available()) { SUCCEED("PipeWire unavailable"); return; } - ensureApp(); - - REQUIRE(tc.client->Test_InsertNode( - MakeNode(100660, "vol-del", "Audio/Sink")).ok()); - - WarpGraphModel model(tc.client.get()); - model.refreshFromClient(); - - auto qtId = model.qtNodeIdForPw(100660); - WarpGraphModel::NodeVolumeState ns; - ns.volume = 0.4f; - model.setNodeVolumeState(qtId, ns); - - REQUIRE(tc.client->Test_RemoveGlobal(100660).ok()); - model.refreshFromClient(); - REQUIRE_FALSE(model.nodeExists(qtId)); - - auto state = model.nodeVolumeState(qtId); - REQUIRE(state.volume == Catch::Approx(1.0f)); - REQUIRE_FALSE(state.mute); -} - -TEST_CASE("GraphEditorWidget has RULES tab") { - auto tc = TestClient::Create(); - if (!tc.available()) { SUCCEED("PipeWire unavailable"); return; } - ensureApp(); - - GraphEditorWidget widget(tc.client.get()); - auto *sidebar = widget.findChild(); - REQUIRE(sidebar != nullptr); - bool found = false; - for (int i = 0; i < sidebar->count(); ++i) { - if (sidebar->tabText(i) == "RULES") { - found = true; - break; - } - } - REQUIRE(found); -} - -TEST_CASE("SetChangeCallback fires on node insert") { - auto tc = TestClient::Create(); - if (!tc.available()) { SUCCEED("PipeWire unavailable"); return; } - - std::atomic count{0}; - tc.client->SetChangeCallback([&count]() { ++count; }); - - REQUIRE(tc.client->Test_InsertNode( - MakeNode(100800, "cb-test-node", "Audio/Sink")).ok()); - REQUIRE(count.load() >= 1); -} - -TEST_CASE("SetChangeCallback fires on node remove") { - auto tc = TestClient::Create(); - if (!tc.available()) { SUCCEED("PipeWire unavailable"); return; } - - REQUIRE(tc.client->Test_InsertNode( - MakeNode(100810, "cb-remove-node", "Audio/Sink")).ok()); - - std::atomic count{0}; - tc.client->SetChangeCallback([&count]() { ++count; }); - - REQUIRE(tc.client->Test_RemoveGlobal(100810).ok()); - REQUIRE(count.load() >= 1); -} - -TEST_CASE("SetChangeCallback can be cleared") { - auto tc = TestClient::Create(); - if (!tc.available()) { SUCCEED("PipeWire unavailable"); return; } - - std::atomic count{0}; - tc.client->SetChangeCallback([&count]() { ++count; }); - tc.client->SetChangeCallback(nullptr); - - REQUIRE(tc.client->Test_InsertNode( - MakeNode(100820, "cb-clear-node", "Audio/Sink")).ok()); - REQUIRE(count.load() == 0); -} - -TEST_CASE("sidebar tab order is METERS MIXER PRESETS RULES") { - auto tc = TestClient::Create(); - if (!tc.available()) { SUCCEED("PipeWire unavailable"); return; } - ensureApp(); - - GraphEditorWidget widget(tc.client.get()); - auto *sidebar = widget.findChild(); - REQUIRE(sidebar != nullptr); - REQUIRE(sidebar->count() >= 4); - REQUIRE(sidebar->tabText(0) == "METERS"); - REQUIRE(sidebar->tabText(1) == "MIXER"); - REQUIRE(sidebar->tabText(2) == "PRESETS"); - REQUIRE(sidebar->tabText(3) == "RULES"); -} diff --git a/tests/warppipe_tests.cpp b/tests/warppipe_tests.cpp index 4b70857..cc3daf3 100644 --- a/tests/warppipe_tests.cpp +++ b/tests/warppipe_tests.cpp @@ -4,7 +4,6 @@ #include #include -#include #include namespace { @@ -793,169 +792,3 @@ TEST_CASE("policy mode does not override user defaults") { REQUIRE(defaults2.value.configured_sink_name == defaults.value.configured_sink_name); REQUIRE(defaults2.value.configured_source_name == defaults.value.configured_source_name); } - -TEST_CASE("Test_SetNodeVolume sets and retrieves volume state") { - auto result = warppipe::Client::Create(DefaultOptions()); - REQUIRE(result.ok()); - auto &client = result.value; - - warppipe::NodeInfo node; - node.id = warppipe::NodeId{900}; - node.name = "vol-sink"; - node.media_class = "Audio/Sink"; - REQUIRE(client->Test_InsertNode(node).ok()); - - auto vol = client->Test_GetNodeVolume(warppipe::NodeId{900}); - REQUIRE(vol.ok()); - REQUIRE(vol.value.volume == Catch::Approx(1.0f)); - REQUIRE(vol.value.mute == false); - - REQUIRE(client->Test_SetNodeVolume(warppipe::NodeId{900}, 0.5f, false).ok()); - vol = client->Test_GetNodeVolume(warppipe::NodeId{900}); - REQUIRE(vol.ok()); - REQUIRE(vol.value.volume == Catch::Approx(0.5f)); - REQUIRE(vol.value.mute == false); - - REQUIRE(client->Test_SetNodeVolume(warppipe::NodeId{900}, 0.75f, true).ok()); - vol = client->Test_GetNodeVolume(warppipe::NodeId{900}); - REQUIRE(vol.ok()); - REQUIRE(vol.value.volume == Catch::Approx(0.75f)); - REQUIRE(vol.value.mute == true); -} - -TEST_CASE("Test_SetNodeVolume clamps volume") { - auto result = warppipe::Client::Create(DefaultOptions()); - REQUIRE(result.ok()); - auto &client = result.value; - - warppipe::NodeInfo node; - node.id = warppipe::NodeId{901}; - node.name = "vol-clamp"; - node.media_class = "Audio/Sink"; - REQUIRE(client->Test_InsertNode(node).ok()); - - REQUIRE(client->Test_SetNodeVolume(warppipe::NodeId{901}, 2.0f, false).ok()); - auto vol = client->Test_GetNodeVolume(warppipe::NodeId{901}); - REQUIRE(vol.ok()); - REQUIRE(vol.value.volume == Catch::Approx(1.5f)); - - REQUIRE(client->Test_SetNodeVolume(warppipe::NodeId{901}, -1.0f, false).ok()); - vol = client->Test_GetNodeVolume(warppipe::NodeId{901}); - REQUIRE(vol.ok()); - REQUIRE(vol.value.volume == Catch::Approx(0.0f)); -} - -TEST_CASE("Test_SetNodeVolume fails for nonexistent node") { - auto result = warppipe::Client::Create(DefaultOptions()); - REQUIRE(result.ok()); - - auto status = result.value->Test_SetNodeVolume(warppipe::NodeId{999}, 0.5f, false); - REQUIRE_FALSE(status.ok()); - REQUIRE(status.code == warppipe::StatusCode::kNotFound); -} - -TEST_CASE("EnsureNodeMeter and NodeMeterPeak round-trip") { - auto result = warppipe::Client::Create(DefaultOptions()); - REQUIRE(result.ok()); - auto &client = result.value; - - warppipe::NodeInfo node; - node.id = warppipe::NodeId{950}; - node.name = "meter-test"; - node.media_class = "Audio/Sink"; - REQUIRE(client->Test_InsertNode(node).ok()); - - REQUIRE(client->EnsureNodeMeter(warppipe::NodeId{950}).ok()); - - auto peak = client->NodeMeterPeak(warppipe::NodeId{950}); - REQUIRE(peak.ok()); - REQUIRE(peak.value.peak_left == Catch::Approx(0.0f)); - REQUIRE(peak.value.peak_right == Catch::Approx(0.0f)); -} - -TEST_CASE("Test_SetNodeMeterPeak updates peaks") { - auto result = warppipe::Client::Create(DefaultOptions()); - REQUIRE(result.ok()); - auto &client = result.value; - - warppipe::NodeInfo node; - node.id = warppipe::NodeId{951}; - node.name = "meter-set"; - node.media_class = "Audio/Sink"; - REQUIRE(client->Test_InsertNode(node).ok()); - - REQUIRE(client->Test_SetNodeMeterPeak(warppipe::NodeId{951}, 0.6f, 0.8f).ok()); - - auto peak = client->NodeMeterPeak(warppipe::NodeId{951}); - REQUIRE(peak.ok()); - REQUIRE(peak.value.peak_left == Catch::Approx(0.6f)); - REQUIRE(peak.value.peak_right == Catch::Approx(0.8f)); -} - -TEST_CASE("DisableNodeMeter removes metering") { - auto result = warppipe::Client::Create(DefaultOptions()); - REQUIRE(result.ok()); - auto &client = result.value; - - warppipe::NodeInfo node; - node.id = warppipe::NodeId{952}; - node.name = "meter-disable"; - node.media_class = "Audio/Sink"; - REQUIRE(client->Test_InsertNode(node).ok()); - REQUIRE(client->EnsureNodeMeter(warppipe::NodeId{952}).ok()); - REQUIRE(client->DisableNodeMeter(warppipe::NodeId{952}).ok()); - - auto peak = client->NodeMeterPeak(warppipe::NodeId{952}); - REQUIRE_FALSE(peak.ok()); - REQUIRE(peak.status.code == warppipe::StatusCode::kNotFound); -} - -TEST_CASE("MasterMeterPeak defaults to zero") { - auto result = warppipe::Client::Create(DefaultOptions()); - REQUIRE(result.ok()); - - auto peak = result.value->MeterPeak(); - REQUIRE(peak.ok()); - REQUIRE(peak.value.peak_left == Catch::Approx(0.0f)); - REQUIRE(peak.value.peak_right == Catch::Approx(0.0f)); -} - -TEST_CASE("Test_SetMasterMeterPeak updates master peaks") { - auto result = warppipe::Client::Create(DefaultOptions()); - REQUIRE(result.ok()); - - REQUIRE(result.value->Test_SetMasterMeterPeak(0.9f, 0.7f).ok()); - - auto peak = result.value->MeterPeak(); - REQUIRE(peak.ok()); - REQUIRE(peak.value.peak_left == Catch::Approx(0.9f)); - REQUIRE(peak.value.peak_right == Catch::Approx(0.7f)); -} - -TEST_CASE("Test_SetNodeMeterPeak clamps values") { - auto result = warppipe::Client::Create(DefaultOptions()); - REQUIRE(result.ok()); - auto &client = result.value; - - warppipe::NodeInfo node; - node.id = warppipe::NodeId{953}; - node.name = "meter-clamp"; - node.media_class = "Audio/Sink"; - REQUIRE(client->Test_InsertNode(node).ok()); - - REQUIRE(client->Test_SetNodeMeterPeak(warppipe::NodeId{953}, 1.5f, -0.5f).ok()); - - auto peak = client->NodeMeterPeak(warppipe::NodeId{953}); - REQUIRE(peak.ok()); - REQUIRE(peak.value.peak_left == Catch::Approx(1.0f)); - REQUIRE(peak.value.peak_right == Catch::Approx(0.0f)); -} - -TEST_CASE("EnsureNodeMeter fails for nonexistent node") { - auto result = warppipe::Client::Create(DefaultOptions()); - REQUIRE(result.ok()); - - auto status = result.value->EnsureNodeMeter(warppipe::NodeId{999}); - REQUIRE_FALSE(status.ok()); - REQUIRE(status.code == warppipe::StatusCode::kNotFound); -}