From 9ac56d0d0b90a4ffb8e2f4e5b2c58b7548dc8347 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Fri, 6 Feb 2026 08:41:55 -0700 Subject: [PATCH] Fix node details --- gui/GraphEditorWidget.cpp | 21 +++--- src/warppipe.cpp | 145 ++++++++++++++++++++++++++++++++++---- 2 files changed, 146 insertions(+), 20 deletions(-) diff --git a/gui/GraphEditorWidget.cpp b/gui/GraphEditorWidget.cpp index 98efd8b..44ce6d8 100644 --- a/gui/GraphEditorWidget.cpp +++ b/gui/GraphEditorWidget.cpp @@ -881,6 +881,9 @@ void GraphEditorWidget::showNodeContextMenu(const QPoint &screenPos, createRuleAction = menu.addAction(QStringLiteral("Create Rule...")); } + menu.addSeparator(); + QAction *detailsAction = menu.addAction(QStringLiteral("Node Details")); + menu.addSeparator(); QAction *pasteAction = menu.addAction(QStringLiteral("Paste")); pasteAction->setShortcut(QKeySequence::Paste); @@ -900,6 +903,10 @@ void GraphEditorWidget::showNodeContextMenu(const QPoint &screenPos, duplicateSelection(); } else if (chosen == deleteAction && m_client) { deleteSelection(); + } else if (chosen == detailsAction) { + m_selectedNodeId = qtNodeId; + updateNodeDetailsPanel(qtNodeId); + m_sidebar->setCurrentWidget(m_nodeDetailsScroll); } else if (chosen == pasteAction) { pasteSelection(QPointF(0, 0)); } else if (chosen == createRuleAction) { @@ -1936,12 +1943,10 @@ void GraphEditorWidget::onSelectionChanged() { if (selected == m_selectedNodeId) return; - if (selected != 0) { - m_selectedNodeId = selected; + m_selectedNodeId = selected; + if (selected != 0 && m_sidebar->currentWidget() == m_nodeDetailsScroll) { updateNodeDetailsPanel(selected); - m_sidebar->setCurrentWidget(m_nodeDetailsScroll); - } else { - m_selectedNodeId = 0; + } else if (selected == 0 && m_sidebar->currentWidget() == m_nodeDetailsScroll) { clearNodeDetailsPanel(); } } @@ -2099,6 +2104,9 @@ void GraphEditorWidget::updateNodeDetailsPanel(QtNodes::NodeId nodeId) { } layout->addWidget(targetCombo); + connect(loopbackCheck, &QCheckBox::toggled, targetCombo, + &QWidget::setEnabled); + if (vnResult.ok() && vnResult.value.loopback) { loopbackCheck->setChecked(true); int idx = targetCombo->findData( @@ -2106,9 +2114,6 @@ void GraphEditorWidget::updateNodeDetailsPanel(QtNodes::NodeId nodeId) { if (idx >= 0) targetCombo->setCurrentIndex(idx); } - - connect(loopbackCheck, &QCheckBox::toggled, targetCombo, - &QWidget::setEnabled); } layout->addSpacing(12); diff --git a/src/warppipe.cpp b/src/warppipe.cpp index 2b687e7..ba06c64 100644 --- a/src/warppipe.cpp +++ b/src/warppipe.cpp @@ -10,6 +10,7 @@ #include #include +#include #include #include #include @@ -104,6 +105,7 @@ bool MatchesRule(const NodeInfo& node, const RuleMatch& match) { struct StreamData { pw_stream* stream = nullptr; + pw_impl_module* module = nullptr; spa_hook listener{}; pw_thread_loop* loop = nullptr; bool is_source = false; @@ -349,6 +351,8 @@ struct Client::Impl { std::unordered_map> node_proxies; std::unordered_map node_channel_counts; + std::unordered_set loopback_internal_nodes; + std::unordered_map meter_states; std::unordered_set metered_nodes; MeterState master_meter; @@ -789,6 +793,7 @@ void Client::Impl::ClearCache() { nodes.clear(); ports.clear(); links.clear(); + loopback_internal_nodes.clear(); pending_auto_links.clear(); auto_link_claimed_pairs.clear(); policy_sync_pending = false; @@ -873,6 +878,103 @@ Result Client::Impl::CreateVirtualStreamLocked(std::string_view name, } } + if (options.behavior == VirtualBehavior::kLoopback && options.target_node) { + std::string args; + args += "{ node.description = \"" + display_name + "\""; + args += " node.name = \"" + stream_name + "\""; + args += " audio.channels = " + std::to_string(options.format.channels); + args += " audio.rate = " + std::to_string(options.format.rate); + args += " capture.props = {"; + args += " target.object = \"" + *options.target_node + "\""; + args += " stream.capture.sink = true"; + args += " node.passive = true"; + args += " node.dont-reconnect = true"; + args += " }"; + args += " playback.props = {"; + args += " node.name = \"" + stream_name + "\""; + args += " node.description = \"" + display_name + "\""; + args += " media.class = \"" + media_class_value + "\""; + args += " node.virtual = true"; + args += " }"; + args += " }"; + + pw_impl_module* mod = pw_context_load_module(context, + "libpipewire-module-loopback", args.c_str(), nullptr); + if (!mod) { + return {Status::Error(StatusCode::kUnavailable, "failed to load loopback module"), 0}; + } + + auto sync_status = SyncLocked(); + if (!sync_status.ok()) { + pw_impl_module_destroy(mod); + return {sync_status, 0}; + } + + uint32_t node_id = SPA_ID_INVALID; + { + std::lock_guard lock(cache_mutex); + for (const auto& entry : nodes) { + if (entry.second.name == stream_name) { + node_id = entry.first; + break; + } + } + } + + if (node_id == SPA_ID_INVALID) { + int wait_attempts = 0; + while (node_id == SPA_ID_INVALID && wait_attempts < 3) { + int wait_res = pw_thread_loop_timed_wait(thread_loop, kSyncWaitSeconds); + if (wait_res == -ETIMEDOUT) { + break; + } + auto sync2 = SyncLocked(); + (void)sync2; + std::lock_guard lock(cache_mutex); + for (const auto& entry : nodes) { + if (entry.second.name == stream_name) { + node_id = entry.first; + break; + } + } + ++wait_attempts; + } + } + + if (node_id == SPA_ID_INVALID) { + pw_impl_module_destroy(mod); + return {Status::Error(StatusCode::kTimeout, "loopback node did not appear in registry"), 0}; + } + + std::string capture_name = "input." + stream_name; + { + std::lock_guard lock(cache_mutex); + for (const auto& entry : nodes) { + if (entry.second.name == capture_name) { + loopback_internal_nodes.insert(entry.first); + break; + } + } + } + + auto stream_data = std::make_unique(); + stream_data->module = mod; + stream_data->loop = thread_loop; + stream_data->is_source = is_source; + stream_data->loopback = true; + stream_data->target_node = *options.target_node; + stream_data->name = stream_name; + stream_data->rate = options.format.rate; + stream_data->channels = options.format.channels; + stream_data->node_id = node_id; + stream_data->ready = true; + { + std::lock_guard lock(cache_mutex); + virtual_streams.emplace(node_id, std::move(stream_data)); + } + return {Status::Ok(), node_id}; + } + pw_properties* props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Audio", PW_KEY_MEDIA_CATEGORY, media_category, PW_KEY_MEDIA_ROLE, "Music", @@ -888,9 +990,6 @@ Result Client::Impl::CreateVirtualStreamLocked(std::string_view name, if (node_group) { pw_properties_set(props, PW_KEY_NODE_GROUP, node_group); } - if (options.behavior == VirtualBehavior::kLoopback && options.target_node) { - pw_properties_set(props, PW_KEY_TARGET_OBJECT, options.target_node->c_str()); - } pw_stream* stream = pw_stream_new(core, stream_name.c_str(), props); if (!stream) { @@ -901,7 +1000,7 @@ Result Client::Impl::CreateVirtualStreamLocked(std::string_view name, stream_data->stream = stream; stream_data->loop = thread_loop; stream_data->is_source = is_source; - stream_data->loopback = options.behavior == VirtualBehavior::kLoopback; + stream_data->loopback = false; if (options.target_node) { stream_data->target_node = *options.target_node; } @@ -926,9 +1025,6 @@ Result Client::Impl::CreateVirtualStreamLocked(std::string_view name, enum pw_direction direction = is_source ? PW_DIRECTION_OUTPUT : PW_DIRECTION_INPUT; enum pw_stream_flags flags = PW_STREAM_FLAG_MAP_BUFFERS; - if (options.behavior == VirtualBehavior::kLoopback && options.target_node) { - flags = static_cast(flags | PW_STREAM_FLAG_AUTOCONNECT); - } int res = pw_stream_connect(stream, direction, PW_ID_ANY, flags, params, 1); if (res < 0) { pw_stream_destroy(stream); @@ -1042,7 +1138,11 @@ void Client::Impl::DisconnectLocked() { } for (auto& entry : streams) { StreamData* stream_data = entry.second.get(); - if (stream_data && stream_data->stream) { + if (!stream_data) continue; + if (stream_data->module) { + pw_impl_module_destroy(stream_data->module); + stream_data->module = nullptr; + } else if (stream_data->stream) { pw_stream_disconnect(stream_data->stream); pw_stream_destroy(stream_data->stream); stream_data->stream = nullptr; @@ -1827,6 +1927,9 @@ Result> Client::ListNodes() { std::vector items; items.reserve(impl_->nodes.size()); for (const auto& entry : impl_->nodes) { + if (impl_->loopback_internal_nodes.count(entry.first)) { + continue; + } NodeInfo info = entry.second; if (!info.is_virtual && impl_->virtual_streams.find(entry.first) != @@ -1933,10 +2036,28 @@ Status Client::RemoveNode(NodeId node) { owned_stream = std::move(it->second); impl_->virtual_streams.erase(it); } - if (owned_stream && owned_stream->stream) { - pw_stream_disconnect(owned_stream->stream); - pw_stream_destroy(owned_stream->stream); - owned_stream->stream = nullptr; + if (owned_stream) { + if (owned_stream->module) { + std::string capture_name = "input." + owned_stream->name; + { + std::lock_guard lock(impl_->cache_mutex); + for (auto it = impl_->loopback_internal_nodes.begin(); + it != impl_->loopback_internal_nodes.end(); ++it) { + auto node_it = impl_->nodes.find(*it); + if (node_it != impl_->nodes.end() && + node_it->second.name == capture_name) { + impl_->loopback_internal_nodes.erase(it); + break; + } + } + } + pw_impl_module_destroy(owned_stream->module); + owned_stream->module = nullptr; + } else if (owned_stream->stream) { + pw_stream_disconnect(owned_stream->stream); + pw_stream_destroy(owned_stream->stream); + owned_stream->stream = nullptr; + } } pw_thread_loop_unlock(impl_->thread_loop); impl_->AutoSave();