diff --git a/CMakeLists.txt b/CMakeLists.txt index 8300ed6..f227e85 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 ) diff --git a/gui/BezierConnectionPainter.cpp b/gui/BezierConnectionPainter.cpp new file mode 100644 index 0000000..93e3414 --- /dev/null +++ b/gui/BezierConnectionPainter.cpp @@ -0,0 +1,120 @@ +#include "BezierConnectionPainter.h" +#include "WarpGraphModel.h" + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +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(&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(base.red() + t * (60 - base.red())); + int g = static_cast(base.green() + t * (210 - base.green())); + int b = static_cast(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(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(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); +} diff --git a/gui/BezierConnectionPainter.h b/gui/BezierConnectionPainter.h new file mode 100644 index 0000000..1c51473 --- /dev/null +++ b/gui/BezierConnectionPainter.h @@ -0,0 +1,15 @@ +#pragma once + +#include + +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; +}; diff --git a/gui/GraphEditorWidget.cpp b/gui/GraphEditorWidget.cpp index 7c26fa9..ebb13a8 100644 --- a/gui/GraphEditorWidget.cpp +++ b/gui/GraphEditorWidget.cpp @@ -1,4 +1,5 @@ #include "AudioLevelMeter.h" +#include "BezierConnectionPainter.h" #include "GraphEditorWidget.h" #include "PresetManager.h" #include "SquareConnectionPainter.h" @@ -9,7 +10,7 @@ #include #include #include -#include + #include #include #include @@ -203,6 +204,8 @@ GraphEditorWidget::GraphEditorWidget(warppipe::Client *client, "UseDataDefinedColors": false }})"); + m_scene->setConnectionPainter(std::make_unique()); + 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()); } else { m_scene->setConnectionPainter( - std::make_unique()); + std::make_unique()); } for (auto *item : m_scene->items()) { diff --git a/gui/WarpGraphModel.cpp b/gui/WarpGraphModel.cpp index e69298b..addf025 100644 --- a/gui/WarpGraphModel.cpp +++ b/gui/WarpGraphModel.cpp @@ -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) { diff --git a/src/warppipe.cpp b/src/warppipe.cpp index 7213d75..ac92d3e 100644 --- a/src/warppipe.cpp +++ b/src/warppipe.cpp @@ -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(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(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(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(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(data); - if (!np || !info || np->params_subscribed) return; + if (!np || !info) return; + + auto* impl = static_cast(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 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 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 Client::Impl::CreateVirtualStreamLocked(std::string_view name, { std::lock_guard 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(); + 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 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_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(); - 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 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 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 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 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,