Fix nodes

This commit is contained in:
Joey Yakimowich-Payne 2026-02-12 16:52:00 -07:00
commit 3c1c86f952
6 changed files with 297 additions and 57 deletions

View file

@ -95,6 +95,7 @@ if(WARPPIPE_BUILD_GUI)
gui/PresetManager.cpp
gui/VolumeWidgets.cpp
gui/AudioLevelMeter.cpp
gui/BezierConnectionPainter.cpp
gui/SquareConnectionPainter.cpp
)
@ -113,6 +114,7 @@ if(WARPPIPE_BUILD_GUI)
gui/PresetManager.cpp
gui/VolumeWidgets.cpp
gui/AudioLevelMeter.cpp
gui/BezierConnectionPainter.cpp
gui/SquareConnectionPainter.cpp
)

View file

@ -0,0 +1,120 @@
#include "BezierConnectionPainter.h"
#include "WarpGraphModel.h"
#include <QtNodes/internal/BasicGraphicsScene.hpp>
#include <QtNodes/internal/ConnectionGraphicsObject.hpp>
#include <QtNodes/internal/ConnectionState.hpp>
#include <QtNodes/internal/Definitions.hpp>
#include <QtNodes/StyleCollection>
#include <QPainter>
#include <QPainterPath>
#include <QPainterPathStroker>
#include <algorithm>
#include <cmath>
QPainterPath BezierConnectionPainter::cubicPath(
QtNodes::ConnectionGraphicsObject const &cgo) const {
QPointF const &in = cgo.endPoint(QtNodes::PortType::In);
QPointF const &out = cgo.endPoint(QtNodes::PortType::Out);
auto const c1c2 = cgo.pointsC1C2();
QPainterPath cubic(out);
cubic.cubicTo(c1c2.first, c1c2.second, in);
return cubic;
}
void BezierConnectionPainter::paint(
QPainter *painter,
QtNodes::ConnectionGraphicsObject const &cgo) const {
auto const &style = QtNodes::StyleCollection::connectionStyle();
bool const hovered = cgo.connectionState().hovered();
bool const selected = cgo.isSelected();
bool const sketch = cgo.connectionState().requiresPort();
auto path = cubicPath(cgo);
float peakLevel = 0.0f;
auto *scene = cgo.nodeScene();
if (scene) {
auto *model = dynamic_cast<WarpGraphModel *>(&scene->graphModel());
if (model) {
auto cId = cgo.connectionId();
peakLevel = std::max(model->nodePeakLevel(cId.outNodeId),
model->nodePeakLevel(cId.inNodeId));
}
}
auto activeColor = [&](QColor base) -> QColor {
if (peakLevel < 0.005f)
return base;
float t = std::min(peakLevel * 2.0f, 1.0f);
int r = static_cast<int>(base.red() + t * (60 - base.red()));
int g = static_cast<int>(base.green() + t * (210 - base.green()));
int b = static_cast<int>(base.blue() + t * (80 - base.blue()));
return QColor(std::clamp(r, 0, 255),
std::clamp(g, 0, 255),
std::clamp(b, 0, 255),
base.alpha());
};
if (hovered || selected) {
QPen pen;
pen.setWidth(static_cast<int>(2 * style.lineWidth()));
pen.setColor(selected ? style.selectedHaloColor() : style.hoveredColor());
painter->setPen(pen);
painter->setBrush(Qt::NoBrush);
painter->drawPath(path);
}
if (sketch) {
QPen pen;
pen.setWidth(static_cast<int>(style.constructionLineWidth()));
pen.setColor(style.constructionColor());
pen.setStyle(Qt::DashLine);
painter->setPen(pen);
painter->setBrush(Qt::NoBrush);
painter->drawPath(path);
} else {
QColor base = selected ? style.selectedColor() : style.normalColor();
QColor color = selected ? base : activeColor(base);
float width = style.lineWidth();
if (!selected && peakLevel > 0.005f)
width += peakLevel * 1.5f;
QPen pen;
pen.setWidthF(width);
pen.setColor(color);
painter->setPen(pen);
painter->setBrush(Qt::NoBrush);
painter->drawPath(path);
}
double const pointRadius = style.pointDiameter() / 2.0;
painter->setPen(style.constructionColor());
painter->setBrush(style.constructionColor());
painter->drawEllipse(cgo.out(), pointRadius, pointRadius);
painter->drawEllipse(cgo.in(), pointRadius, pointRadius);
}
QPainterPath BezierConnectionPainter::getPainterStroke(
QtNodes::ConnectionGraphicsObject const &cgo) const {
auto cubic = cubicPath(cgo);
QPointF const &out = cgo.endPoint(QtNodes::PortType::Out);
QPainterPath result(out);
unsigned int constexpr segments = 20;
for (unsigned int i = 0; i < segments; ++i) {
double ratio = double(i + 1) / segments;
result.lineTo(cubic.pointAtPercent(ratio));
}
QPainterPathStroker stroker;
stroker.setWidth(10.0);
return stroker.createStroke(result);
}

View file

@ -0,0 +1,15 @@
#pragma once
#include <QtNodes/internal/AbstractConnectionPainter.hpp>
class BezierConnectionPainter : public QtNodes::AbstractConnectionPainter {
public:
void paint(QPainter *painter,
QtNodes::ConnectionGraphicsObject const &cgo) const override;
QPainterPath
getPainterStroke(QtNodes::ConnectionGraphicsObject const &cgo) const override;
private:
QPainterPath
cubicPath(QtNodes::ConnectionGraphicsObject const &cgo) const;
};

View file

@ -1,4 +1,5 @@
#include "AudioLevelMeter.h"
#include "BezierConnectionPainter.h"
#include "GraphEditorWidget.h"
#include "PresetManager.h"
#include "SquareConnectionPainter.h"
@ -9,7 +10,7 @@
#include <QtNodes/BasicGraphicsScene>
#include <QtNodes/ConnectionStyle>
#include <QtNodes/GraphicsView>
#include <QtNodes/internal/DefaultConnectionPainter.hpp>
#include <QtNodes/internal/NodeGraphicsObject.hpp>
#include <QtNodes/internal/ConnectionGraphicsObject.hpp>
#include <QtNodes/internal/UndoCommands.hpp>
@ -203,6 +204,8 @@ GraphEditorWidget::GraphEditorWidget(warppipe::Client *client,
"UseDataDefinedColors": false
}})");
m_scene->setConnectionPainter(std::make_unique<BezierConnectionPainter>());
m_view = new ZoomGraphicsView(m_scene);
m_view->setFocusPolicy(Qt::StrongFocus);
m_view->viewport()->setFocusPolicy(Qt::StrongFocus);
@ -1927,7 +1930,7 @@ void GraphEditorWidget::setConnectionStyle(ConnectionStyleType style) {
m_scene->setConnectionPainter(std::make_unique<SquareConnectionPainter>());
} else {
m_scene->setConnectionPainter(
std::make_unique<QtNodes::DefaultConnectionPainter>());
std::make_unique<BezierConnectionPainter>());
}
for (auto *item : m_scene->items()) {

View file

@ -481,8 +481,14 @@ void WarpGraphModel::refreshFromClient() {
if (existing != m_pwToQt.end()) {
QtNodes::NodeId qtId = existing->second;
auto &data = m_nodes[qtId];
bool typeChanged = (data.info.is_virtual != nodeInfo.is_virtual);
data.info = nodeInfo;
if (typeChanged) {
m_styleCache.erase(qtId);
Q_EMIT nodeUpdated(qtId);
}
bool portsMissing =
data.inputPorts.empty() && data.outputPorts.empty();
if (portsMissing) {

View file

@ -107,10 +107,12 @@ bool MatchesRule(const NodeInfo& node, const RuleMatch& match) {
struct StreamData {
pw_stream* stream = nullptr;
pw_impl_module* module = nullptr;
pw_proxy* proxy = nullptr;
spa_hook listener{};
pw_thread_loop* loop = nullptr;
bool is_source = false;
bool loopback = false;
bool linger = false;
std::string target_node;
std::string name;
bool ready = false;
@ -175,6 +177,42 @@ static const pw_proxy_events kLinkProxyEvents = {
.error = LinkProxyError,
};
void NodeProxyBound(void* data, uint32_t global_id) {
auto* sd = static_cast<StreamData*>(data);
if (!sd) return;
sd->node_id = global_id;
sd->ready = true;
if (sd->loop) {
pw_thread_loop_signal(sd->loop, false);
}
}
void NodeProxyRemoved(void* data) {
auto* sd = static_cast<StreamData*>(data);
if (!sd) return;
sd->ready = true;
if (sd->loop) {
pw_thread_loop_signal(sd->loop, false);
}
}
void NodeProxyError(void* data, int, int res, const char* message) {
auto* sd = static_cast<StreamData*>(data);
if (!sd) return;
sd->failed = true;
sd->error = message ? message : spa_strerror(res);
if (sd->loop) {
pw_thread_loop_signal(sd->loop, false);
}
}
static const pw_proxy_events kNodeProxyEvents = {
.version = PW_VERSION_PROXY_EVENTS,
.bound = NodeProxyBound,
.removed = NodeProxyRemoved,
.error = NodeProxyError,
};
void StreamProcess(void* data) {
auto* stream_data = static_cast<StreamData*>(data);
if (!stream_data || !stream_data->stream) {
@ -435,7 +473,29 @@ struct Client::Impl {
void Client::Impl::NodeInfoChanged(void* data, const struct pw_node_info* info) {
auto* np = static_cast<NodeProxyData*>(data);
if (!np || !info || np->params_subscribed) return;
if (!np || !info) return;
auto* impl = static_cast<Client::Impl*>(np->impl_ptr);
bool notify = false;
if (impl && info->props) {
const char* virt_str = spa_dict_lookup(info->props, PW_KEY_NODE_VIRTUAL);
if (virt_str) {
std::lock_guard<std::mutex> lock(impl->cache_mutex);
auto it = impl->nodes.find(np->node_id);
if (it != impl->nodes.end()) {
bool new_virtual = spa_streq(virt_str, "true");
if (it->second.is_virtual != new_virtual) {
it->second.is_virtual = new_virtual;
notify = true;
}
}
}
}
if (notify) {
impl->NotifyChange();
}
if (np->params_subscribed) return;
for (uint32_t i = 0; i < info->n_params; ++i) {
if (info->params[i].id == SPA_PARAM_Props &&
@ -508,15 +568,18 @@ void Client::Impl::NodeParamChanged(void* data, int, uint32_t id,
}
}
bool is_virtual = false;
bool uses_monitor_volume = false;
{
std::lock_guard<std::mutex> lock(impl->cache_mutex);
is_virtual = impl->virtual_streams.count(np->node_id) > 0;
auto node_it = impl->nodes.find(np->node_id);
if (node_it != impl->nodes.end()) {
uses_monitor_volume = node_it->second.is_virtual;
}
}
float effective_vol = is_virtual && mon_volume >= 0.0f ? mon_volume : volume;
bool effective_mute = is_virtual && found_mon_mute ? mon_mute : mute;
bool effective_found_mute = is_virtual ? found_mon_mute : found_mute;
float effective_vol = uses_monitor_volume && mon_volume >= 0.0f ? mon_volume : volume;
bool effective_mute = uses_monitor_volume && found_mon_mute ? mon_mute : mute;
bool effective_found_mute = uses_monitor_volume ? found_mon_mute : found_mute;
bool changed = false;
{
@ -855,16 +918,27 @@ Result<uint32_t> Client::Impl::CreateVirtualStreamLocked(std::string_view name,
{
std::lock_guard<std::mutex> lock(cache_mutex);
for (const auto& entry : nodes) {
if (entry.second.name == stream_name) {
return {Status::Error(StatusCode::kInvalidArgument, "duplicate node name"), 0};
}
}
for (const auto& entry : virtual_streams) {
if (entry.second && entry.second->name == stream_name) {
return {Status::Error(StatusCode::kInvalidArgument, "duplicate node name"), 0};
}
}
for (const auto& entry : nodes) {
if (entry.second.name == stream_name) {
uint32_t existing_id = entry.first;
auto adopted = std::make_unique<StreamData>();
adopted->linger = true;
adopted->loop = thread_loop;
adopted->is_source = is_source;
adopted->name = stream_name;
adopted->node_id = existing_id;
adopted->ready = true;
if (options.format.rate != 0) adopted->rate = options.format.rate;
if (options.format.channels != 0) adopted->channels = options.format.channels;
virtual_streams.emplace(existing_id, std::move(adopted));
return {Status::Ok(), existing_id};
}
}
if (options.behavior == VirtualBehavior::kLoopback && options.target_node) {
bool found_target = false;
for (const auto& entry : nodes) {
@ -976,29 +1050,53 @@ Result<uint32_t> Client::Impl::CreateVirtualStreamLocked(std::string_view name,
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",
PW_KEY_MEDIA_CLASS, media_class_value.c_str(),
PW_KEY_NODE_NAME, stream_name.c_str(),
PW_KEY_MEDIA_NAME, display_name.c_str(),
PW_KEY_NODE_DESCRIPTION, display_name.c_str(),
PW_KEY_NODE_VIRTUAL, "true",
nullptr);
// Build audio position string for channels (e.g. "FL,FR" for stereo).
std::string audio_position;
{
static const char* const kChannelNames[] = {
"FL", "FR", "FC", "LFE", "RL", "RR", "SL", "SR"
};
uint32_t ch = options.format.channels;
for (uint32_t i = 0; i < ch && i < 8; ++i) {
if (i > 0) audio_position += ',';
audio_position += kChannelNames[i];
}
if (audio_position.empty()) audio_position = "FL,FR";
}
pw_properties* props = pw_properties_new(
"factory.name", "support.null-audio-sink",
PW_KEY_NODE_NAME, stream_name.c_str(),
PW_KEY_NODE_DESCRIPTION, display_name.c_str(),
PW_KEY_MEDIA_CLASS, media_class_value.c_str(),
PW_KEY_NODE_VIRTUAL, "true",
"audio.position", audio_position.c_str(),
nullptr);
#ifndef WARPPIPE_TESTING
pw_properties_set(props, PW_KEY_OBJECT_LINGER, "true");
#endif
if (!props) {
return {Status::Error(StatusCode::kInternal, "failed to allocate stream properties"), 0};
return {Status::Error(StatusCode::kInternal, "failed to allocate node properties"), 0};
}
if (node_group) {
pw_properties_set(props, PW_KEY_NODE_GROUP, node_group);
}
pw_stream* stream = pw_stream_new(core, stream_name.c_str(), props);
if (!stream) {
return {Status::Error(StatusCode::kUnavailable, "failed to create pipewire stream"), 0};
pw_proxy* proxy = reinterpret_cast<pw_proxy*>(
pw_core_create_object(core, "adapter",
PW_TYPE_INTERFACE_Node,
PW_VERSION_NODE,
&props->dict, 0));
pw_properties_free(props);
if (!proxy) {
return {Status::Error(StatusCode::kUnavailable, "failed to create virtual node"), 0};
}
auto stream_data = std::make_unique<StreamData>();
stream_data->stream = stream;
stream_data->proxy = proxy;
#ifndef WARPPIPE_TESTING
stream_data->linger = true;
#endif
stream_data->loop = thread_loop;
stream_data->is_source = is_source;
stream_data->loopback = false;
@ -1013,28 +1111,11 @@ Result<uint32_t> Client::Impl::CreateVirtualStreamLocked(std::string_view name,
stream_data->channels = options.format.channels;
}
pw_stream_add_listener(stream, &stream_data->listener, &kStreamEvents, stream_data.get());
pw_proxy_add_listener(proxy, &stream_data->listener, &kNodeProxyEvents, stream_data.get());
const struct spa_pod* params[1];
uint8_t buffer[1024];
spa_pod_builder builder = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
spa_audio_info_raw audio_info{};
audio_info.format = SPA_AUDIO_FORMAT_F32;
audio_info.rate = stream_data->rate;
audio_info.channels = stream_data->channels;
params[0] = spa_format_audio_raw_build(&builder, SPA_PARAM_EnumFormat, &audio_info);
enum pw_direction direction = is_source ? PW_DIRECTION_OUTPUT : PW_DIRECTION_INPUT;
enum pw_stream_flags flags = PW_STREAM_FLAG_MAP_BUFFERS;
int res = pw_stream_connect(stream, direction, PW_ID_ANY, flags, params, 1);
if (res < 0) {
pw_stream_destroy(stream);
return {Status::Error(StatusCode::kUnavailable, "failed to connect pipewire stream"), 0};
}
uint32_t node_id = pw_stream_get_node_id(stream);
uint32_t node_id = SPA_ID_INVALID;
int wait_attempts = 0;
while (node_id == SPA_ID_INVALID && !stream_data->failed && wait_attempts < 3) {
while (node_id == SPA_ID_INVALID && !stream_data->failed && !stream_data->ready && wait_attempts < 3) {
int wait_res = pw_thread_loop_timed_wait(thread_loop, kSyncWaitSeconds);
if (wait_res == -ETIMEDOUT) {
break;
@ -1044,17 +1125,17 @@ Result<uint32_t> Client::Impl::CreateVirtualStreamLocked(std::string_view name,
}
if (stream_data->failed) {
std::string error = stream_data->error.empty() ? "stream entered error state" : stream_data->error;
pw_stream_destroy(stream);
std::string error = stream_data->error.empty() ? "node creation failed" : stream_data->error;
pw_proxy_destroy(proxy);
return {Status::Error(StatusCode::kUnavailable, std::move(error)), 0};
}
node_id = stream_data->node_id;
if (node_id == SPA_ID_INVALID) {
pw_stream_destroy(stream);
return {Status::Error(StatusCode::kTimeout, "timed out waiting for stream node id"), 0};
pw_proxy_destroy(proxy);
return {Status::Error(StatusCode::kTimeout, "timed out waiting for virtual node id"), 0};
}
stream_data->node_id = node_id;
stream_data->ready = true;
{
std::lock_guard<std::mutex> lock(cache_mutex);
@ -1140,9 +1221,15 @@ void Client::Impl::DisconnectLocked() {
for (auto& entry : streams) {
StreamData* stream_data = entry.second.get();
if (!stream_data) continue;
if (stream_data->module) {
if (stream_data->linger && stream_data->proxy) {
spa_hook_remove(&stream_data->listener);
stream_data->proxy = nullptr;
} else if (stream_data->module) {
pw_impl_module_destroy(stream_data->module);
stream_data->module = nullptr;
} else if (stream_data->proxy) {
pw_proxy_destroy(stream_data->proxy);
stream_data->proxy = nullptr;
} else if (stream_data->stream) {
pw_stream_disconnect(stream_data->stream);
pw_stream_destroy(stream_data->stream);
@ -2113,6 +2200,9 @@ Status Client::RemoveNode(NodeId node) {
}
pw_impl_module_destroy(owned_stream->module);
owned_stream->module = nullptr;
} else if (owned_stream->proxy) {
pw_proxy_destroy(owned_stream->proxy);
owned_stream->proxy = nullptr;
} else if (owned_stream->stream) {
pw_stream_disconnect(owned_stream->stream);
pw_stream_destroy(owned_stream->stream);
@ -2137,12 +2227,15 @@ Status Client::SetNodeVolume(NodeId node, float volume, bool mute) {
pw_thread_loop_lock(impl_->thread_loop);
bool is_virtual = false;
{
std::lock_guard<std::mutex> lock(impl_->cache_mutex);
if (impl_->nodes.find(node.value) == impl_->nodes.end()) {
auto node_it = impl_->nodes.find(node.value);
if (node_it == impl_->nodes.end()) {
pw_thread_loop_unlock(impl_->thread_loop);
return Status::Error(StatusCode::kNotFound, "node not found");
}
is_virtual = node_it->second.is_virtual;
}
uint32_t n_channels;
@ -2154,7 +2247,8 @@ Status Client::SetNodeVolume(NodeId node, float volume, bool mute) {
if (streamIt != impl_->virtual_streams.end() && streamIt->second->stream) {
own_stream = streamIt->second->stream;
n_channels = streamIt->second->channels;
} else {
}
if (!own_stream) {
auto proxyIt = impl_->node_proxies.find(node.value);
if (proxyIt == impl_->node_proxies.end() || !proxyIt->second->proxy) {
pw_thread_loop_unlock(impl_->thread_loop);
@ -2170,8 +2264,8 @@ Status Client::SetNodeVolume(NodeId node, float volume, bool mute) {
uint8_t buffer[512];
spa_pod_builder builder = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
uint32_t vol_prop = own_stream ? SPA_PROP_monitorVolumes : SPA_PROP_channelVolumes;
uint32_t mute_prop = own_stream ? SPA_PROP_monitorMute : SPA_PROP_mute;
uint32_t vol_prop = is_virtual ? SPA_PROP_monitorVolumes : SPA_PROP_channelVolumes;
uint32_t mute_prop = is_virtual ? SPA_PROP_monitorMute : SPA_PROP_mute;
spa_pod_frame obj_frame;
spa_pod_builder_push_object(&builder, &obj_frame,