From 87e5aca9d8cd39aaa4c669f2b7e4d3a5821fa5d6 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Tue, 27 Jan 2026 16:41:51 -0700 Subject: [PATCH] Nodes --- CMakeLists.txt | 1 + src/gui/GraphEditorWidget.cpp | 108 +++++++++++++++++++- src/gui/GraphEditorWidget.h | 7 ++ src/gui/PipeWireGraphModel.cpp | 79 +++++++++++++- src/gui/PipeWireGraphModel.h | 1 + src/main_gui.cpp | 39 ++++--- src/meters/AudioLevelMeter.cpp | 65 ++++++++++++ src/meters/AudioLevelMeter.h | 24 +++++ src/pipewire/nodeinfo.h | 7 +- src/pipewire/pipewirecontroller.cpp | 153 ++++++++++++++++++++++++++-- src/pipewire/pipewirecontroller.h | 8 ++ 11 files changed, 459 insertions(+), 33 deletions(-) create mode 100644 src/meters/AudioLevelMeter.cpp create mode 100644 src/meters/AudioLevelMeter.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 88a9625..4884b4c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -89,6 +89,7 @@ add_executable(potato-gui src/main_gui.cpp src/gui/GraphEditorWidget.cpp src/gui/PipeWireGraphModel.cpp + src/meters/AudioLevelMeter.cpp ) target_link_libraries(potato-gui PRIVATE diff --git a/src/gui/GraphEditorWidget.cpp b/src/gui/GraphEditorWidget.cpp index 77acc55..5468c5f 100644 --- a/src/gui/GraphEditorWidget.cpp +++ b/src/gui/GraphEditorWidget.cpp @@ -1,22 +1,84 @@ #include "GraphEditorWidget.h" +#include "meters/AudioLevelMeter.h" #include #include +#include #include +#include +#include +#include #include +#include +#include + GraphEditorWidget::GraphEditorWidget(Potato::PipeWireController *controller, QWidget *parent) : QWidget(parent) , m_controller(controller) { + const QString nodeStyleJson = R"JSON({ + "NodeStyle": { + "NormalBoundaryColor": [62, 68, 80], + "SelectedBoundaryColor": [42, 132, 194], + "GradientColor0": [214, 220, 230], + "GradientColor1": [202, 208, 220], + "GradientColor2": [192, 198, 210], + "GradientColor3": [182, 188, 200], + "ShadowColor": [0, 0, 0], + "FontColor": [28, 32, 38], + "FontColorFaded": [72, 78, 88], + "ConnectionPointColor": [62, 68, 80], + "FilledConnectionPointColor": [42, 132, 194], + "WarningColor": [196, 140, 45], + "ErrorColor": [196, 64, 64], + "PenWidth": 1.4, + "HoveredPenWidth": 2.2, + "ConnectionPointDiameter": 9.0, + "Opacity": 1.0 + } + })JSON"; + QtNodes::NodeStyle::setNodeStyle(nodeStyleJson); + + const QString viewStyleJson = R"JSON({ + "GraphicsViewStyle": { + "BackgroundColor": [26, 28, 34], + "FineGridColor": [46, 50, 60], + "CoarseGridColor": [66, 72, 86] + } + })JSON"; + QtNodes::GraphicsViewStyle::setStyle(viewStyleJson); + m_model = new PipeWireGraphModel(controller, this); m_model->loadLayout(); m_scene = new QtNodes::BasicGraphicsScene(*m_model, this); m_view = new QtNodes::GraphicsView(m_scene); + m_scene->setBackgroundBrush(QColor(42, 45, 52)); + + auto *splitter = new QSplitter(this); + splitter->setOrientation(Qt::Horizontal); + splitter->addWidget(m_view); + + auto *meterPanel = new QWidget(splitter); + auto *meterLayout = new QVBoxLayout(meterPanel); + meterLayout->setContentsMargins(12, 12, 12, 12); + meterLayout->setSpacing(10); + + auto *meterLabel = new QLabel(QString("Master Meter"), meterPanel); + meterLayout->addWidget(meterLabel); + + m_meter = new AudioLevelMeter(meterPanel); + meterLayout->addWidget(m_meter, 1); + meterLayout->addStretch(); + + meterPanel->setFixedWidth(140); + splitter->addWidget(meterPanel); + splitter->setStretchFactor(0, 1); + splitter->setStretchFactor(1, 0); auto *layout = new QVBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); - layout->addWidget(m_view); + layout->addWidget(splitter); setLayout(layout); connect(m_model, &PipeWireGraphModel::connectionCreated, @@ -26,6 +88,8 @@ GraphEditorWidget::GraphEditorWidget(Potato::PipeWireController *controller, QWi connect(m_controller, &Potato::PipeWireController::nodeAdded, this, &GraphEditorWidget::onNodeAdded); + connect(m_controller, &Potato::PipeWireController::nodeChanged, + this, &GraphEditorWidget::onNodeChanged); connect(m_controller, &Potato::PipeWireController::nodeRemoved, this, &GraphEditorWidget::onNodeRemoved); connect(m_controller, &Potato::PipeWireController::linkAdded, @@ -92,13 +156,28 @@ GraphEditorWidget::GraphEditorWidget(Potato::PipeWireController *controller, QWi m_model->saveLayout(); } }); + + m_meterTimer = new QTimer(this); + m_meterTimer->setInterval(33); + m_meterTimer->setTimerType(Qt::PreciseTimer); + connect(m_meterTimer, &QTimer::timeout, this, &GraphEditorWidget::updateMeter); + m_meterTimer->start(); +} + +static bool isAudioEndpoint(const Potato::NodeInfo &node) +{ + return node.mediaClass == Potato::MediaClass::AudioSink + || node.mediaClass == Potato::MediaClass::AudioSource + || node.mediaClass == Potato::MediaClass::AudioDuplex; } void GraphEditorWidget::syncGraph() { const QVector nodes = m_controller->nodes(); for (const auto &node : nodes) { - m_model->addPipeWireNode(node); + if (isAudioEndpoint(node)) { + m_model->addPipeWireNode(node); + } } const QVector links = m_controller->links(); @@ -124,7 +203,20 @@ void GraphEditorWidget::refreshGraph() void GraphEditorWidget::onNodeAdded(const Potato::NodeInfo &node) { - m_model->addPipeWireNode(node); + if (isAudioEndpoint(node)) { + m_model->addPipeWireNode(node); + } +} + +void GraphEditorWidget::onNodeChanged(const Potato::NodeInfo &node) +{ + if (!isAudioEndpoint(node)) { + return; + } + + if (!m_model->updatePipeWireNode(node)) { + m_model->addPipeWireNode(node); + } } void GraphEditorWidget::onNodeRemoved(uint32_t nodeId) @@ -234,3 +326,13 @@ QString GraphEditorWidget::connectionKey(const QtNodes::ConnectionId &connection + QString(":") + QString::number(connectionId.inNodeId) + QString(":") + QString::number(connectionId.inPortIndex); } + +void GraphEditorWidget::updateMeter() +{ + if (!m_meter) { + return; + } + + const float peak = m_controller->meterPeak(); + m_meter->setLevel(peak); +} diff --git a/src/gui/GraphEditorWidget.h b/src/gui/GraphEditorWidget.h index d04abae..688d51f 100644 --- a/src/gui/GraphEditorWidget.h +++ b/src/gui/GraphEditorWidget.h @@ -11,6 +11,9 @@ #include #include +class AudioLevelMeter; +class QTimer; + class GraphEditorWidget : public QWidget { Q_OBJECT @@ -20,11 +23,13 @@ public: private slots: void onNodeAdded(const Potato::NodeInfo &node); + void onNodeChanged(const Potato::NodeInfo &node); void onNodeRemoved(uint32_t nodeId); void onLinkAdded(const Potato::LinkInfo &link); void onLinkRemoved(uint32_t linkId); void onConnectionCreated(QtNodes::ConnectionId const connectionId); void onConnectionDeleted(QtNodes::ConnectionId const connectionId); + void updateMeter(); private: void syncGraph(); @@ -40,4 +45,6 @@ private: QSet m_ignoreDelete; QMap m_connectionToLinkId; QMap m_linkIdToConnection; + AudioLevelMeter *m_meter = nullptr; + QTimer *m_meterTimer = nullptr; }; diff --git a/src/gui/PipeWireGraphModel.cpp b/src/gui/PipeWireGraphModel.cpp index 4cd0776..e1ea200 100644 --- a/src/gui/PipeWireGraphModel.cpp +++ b/src/gui/PipeWireGraphModel.cpp @@ -1,19 +1,56 @@ #include "PipeWireGraphModel.h" +#include "PipeWireGraphModel.h" #include #include #include #include #include +#include +#include #include #include #include +#include + #include #include #include +namespace { +int nodeWidthFor(const Potato::NodeInfo &info) +{ + QFont captionFont; + captionFont.setBold(true); + QFontMetrics captionMetrics(captionFont); + int maxTextWidth = captionMetrics.horizontalAdvance(info.name); + + QFont portFont; + QFontMetrics portMetrics(portFont); + for (const auto &port : info.inputPorts) { + maxTextWidth = std::max(maxTextWidth, portMetrics.horizontalAdvance(port.name)); + } + for (const auto &port : info.outputPorts) { + maxTextWidth = std::max(maxTextWidth, portMetrics.horizontalAdvance(port.name)); + } + + const int widthPadding = 140; + const int minWidth = 300; + const int maxWidth = 520; + const int width = maxTextWidth + widthPadding; + return std::max(minWidth, std::min(maxWidth, width)); +} + +QString elideLabel(const QString &text, int width, const QFont &font) +{ + QFontMetrics metrics(font); + const int available = std::max(60, width); + return metrics.elidedText(text, Qt::ElideRight, available); +} +} + PipeWireGraphModel::PipeWireGraphModel(Potato::PipeWireController *controller, QObject *parent) : QtNodes::AbstractGraphModel() , m_controller(controller) @@ -154,7 +191,12 @@ QVariant PipeWireGraphModel::nodeData(QtNodes::NodeId nodeId, QtNodes::NodeRole switch (role) { case QtNodes::NodeRole::Caption: - return info.name; + { + QFont captionFont; + captionFont.setBold(true); + const int width = nodeWidthFor(info) - 40; + return elideLabel(info.name, width, captionFont); + } case QtNodes::NodeRole::CaptionVisible: return true; case QtNodes::NodeRole::Position: { @@ -165,13 +207,24 @@ QVariant PipeWireGraphModel::nodeData(QtNodes::NodeId nodeId, QtNodes::NodeRole return QPointF(0, 0); } case QtNodes::NodeRole::Size: - return QSize(180, 80); + { + const int maxPorts = std::max(info.inputPorts.size(), info.outputPorts.size()); + const int baseHeight = 70; + const int perPortHeight = 28; + const int height = std::max(110, baseHeight + (maxPorts * perPortHeight)); + + const int width = nodeWidthFor(info); + + return QSize(width, height); + } case QtNodes::NodeRole::InPortCount: return static_cast(info.inputPorts.size()); case QtNodes::NodeRole::OutPortCount: return static_cast(info.outputPorts.size()); case QtNodes::NodeRole::Type: return QString("PipeWire"); + case QtNodes::NodeRole::Style: + return QtNodes::StyleCollection::nodeStyle().toJson().toVariantMap(); default: return QVariant(); } @@ -217,11 +270,15 @@ QVariant PipeWireGraphModel::portData(QtNodes::NodeId nodeId, if (role == QtNodes::PortRole::Caption) { if (portType == QtNodes::PortType::In) { if (portIndex < static_cast(info.inputPorts.size())) { - return portLabel(info.inputPorts.at(portIndex)); + QFont font; + const int width = nodeWidthFor(info) - 80; + return elideLabel(info.inputPorts.at(portIndex).name, width, font); } } else if (portType == QtNodes::PortType::Out) { if (portIndex < static_cast(info.outputPorts.size())) { - return portLabel(info.outputPorts.at(portIndex)); + QFont font; + const int width = nodeWidthFor(info) - 80; + return elideLabel(info.outputPorts.at(portIndex).name, width, font); } } } @@ -361,6 +418,19 @@ bool PipeWireGraphModel::findConnectionForLink(uint32_t linkId, QtNodes::Connect return true; } +bool PipeWireGraphModel::updatePipeWireNode(const Potato::NodeInfo &node) +{ + auto it = m_pwToNode.find(node.id); + if (it == m_pwToNode.end()) { + return false; + } + + const QtNodes::NodeId nodeId = it->second; + m_nodes[nodeId] = node; + Q_EMIT nodeUpdated(nodeId); + return true; +} + const Potato::NodeInfo *PipeWireGraphModel::nodeInfo(QtNodes::NodeId nodeId) const { auto it = m_nodes.find(nodeId); @@ -650,3 +720,4 @@ bool PipeWireGraphModel::viewState(double &scale, QPointF ¢er) const center = m_viewCenter; return true; } + #include diff --git a/src/gui/PipeWireGraphModel.h b/src/gui/PipeWireGraphModel.h index 7ffd83e..3ea7a6b 100644 --- a/src/gui/PipeWireGraphModel.h +++ b/src/gui/PipeWireGraphModel.h @@ -56,6 +56,7 @@ public: bool findConnectionForLink(uint32_t linkId, QtNodes::ConnectionId &connectionId) const; const Potato::NodeInfo *nodeInfo(QtNodes::NodeId nodeId) const; bool connectionIdForLink(const Potato::LinkInfo &link, QtNodes::ConnectionId &connectionId) const; + bool updatePipeWireNode(const Potato::NodeInfo &node); void reset(); void loadLayout(); void saveLayout() const; diff --git a/src/main_gui.cpp b/src/main_gui.cpp index e41877a..02fb029 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -44,11 +45,6 @@ int main(int argc, char *argv[]) auto *screenshotAction = new QAction(&window); screenshotAction->setShortcut(QKeySequence(Qt::Key_F12)); QObject::connect(screenshotAction, &QAction::triggered, [&window]() { - QScreen *screen = window.screen(); - if (!screen) { - return; - } - const QString baseDir = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation) + QString("/potato"); QDir().mkpath(baseDir); @@ -57,8 +53,17 @@ int main(int argc, char *argv[]) .arg(QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss")); const QString filePath = baseDir + QString("/") + fileName; - const QPixmap pixmap = screen->grabWindow(window.winId()); - pixmap.save(filePath, "PNG"); + QPixmap pixmap = window.grab(); + if (pixmap.isNull()) { + QScreen *screen = window.screen(); + if (screen) { + pixmap = screen->grabWindow(window.winId()); + } + } + + if (!pixmap.isNull()) { + pixmap.save(filePath, "PNG"); + } }); window.addAction(screenshotAction); @@ -71,15 +76,21 @@ int main(int argc, char *argv[]) const QString screenshotPath = parser.value(screenshotOption); if (!screenshotPath.isEmpty()) { - QTimer::singleShot(500, &window, [&window, &parser, screenshotPath]() { - QScreen *screen = window.screen(); - if (!screen) { - QCoreApplication::exit(2); - return; + QTimer::singleShot(800, &window, [&window, &parser, screenshotPath]() { + const QFileInfo info(screenshotPath); + if (!info.absolutePath().isEmpty()) { + QDir().mkpath(info.absolutePath()); } - const QPixmap pixmap = screen->grabWindow(window.winId()); - if (!pixmap.save(screenshotPath)) { + QPixmap pixmap = window.grab(); + if (pixmap.isNull()) { + QScreen *screen = window.screen(); + if (screen) { + pixmap = screen->grabWindow(window.winId()); + } + } + + if (pixmap.isNull() || !pixmap.save(screenshotPath)) { QCoreApplication::exit(3); return; } diff --git a/src/meters/AudioLevelMeter.cpp b/src/meters/AudioLevelMeter.cpp new file mode 100644 index 0000000..72410cc --- /dev/null +++ b/src/meters/AudioLevelMeter.cpp @@ -0,0 +1,65 @@ +#include "AudioLevelMeter.h" + +#include +#include + +AudioLevelMeter::AudioLevelMeter(QWidget *parent) + : QWidget(parent) +{ + setMinimumWidth(36); + setMinimumHeight(120); +} + +void AudioLevelMeter::setLevel(float level) +{ + const float clamped = qBound(0.0f, level, 1.0f); + m_level = clamped; + if (clamped >= m_holdLevel) { + m_holdLevel = clamped; + m_holdFrames = 6; + } else if (m_holdFrames > 0) { + --m_holdFrames; + } else { + m_holdLevel = qMax(0.0f, m_holdLevel - m_decayPerFrame); + } + update(); +} + +QSize AudioLevelMeter::sizeHint() const +{ + return QSize(40, 160); +} + +void AudioLevelMeter::paintEvent(QPaintEvent *event) +{ + Q_UNUSED(event) + + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing, false); + + const QRectF bounds = rect(); + painter.fillRect(bounds, QColor(24, 24, 28)); + + const qreal height = bounds.height(); + const qreal filled = height * m_level; + const QRectF barRect(bounds.left() + 6.0, bounds.bottom() - filled, + bounds.width() - 12.0, filled); + + QColor color(50, 255, 120); + if (m_level > 0.9f) { + color = QColor(255, 70, 70); + } else if (m_level > 0.7f) { + color = QColor(255, 200, 70); + } + + painter.fillRect(barRect, color); + + if (m_holdLevel > 0.0f) { + const qreal holdY = bounds.bottom() - (height * m_holdLevel); + painter.setPen(QPen(QColor(240, 240, 240), 2)); + painter.drawLine(QPointF(bounds.left() + 4.0, holdY), + QPointF(bounds.right() - 4.0, holdY)); + } + painter.setPen(QColor(45, 45, 50)); + painter.drawRect(bounds.adjusted(1, 1, -2, -2)); +} diff --git a/src/meters/AudioLevelMeter.h b/src/meters/AudioLevelMeter.h new file mode 100644 index 0000000..c7fa9be --- /dev/null +++ b/src/meters/AudioLevelMeter.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include + +class AudioLevelMeter : public QWidget +{ + Q_OBJECT + +public: + explicit AudioLevelMeter(QWidget *parent = nullptr); + + void setLevel(float level); + QSize sizeHint() const override; + +protected: + void paintEvent(QPaintEvent *event) override; + +private: + float m_level = 0.0f; + float m_holdLevel = 0.0f; + int m_holdFrames = 0; + float m_decayPerFrame = 0.02f; +}; diff --git a/src/pipewire/nodeinfo.h b/src/pipewire/nodeinfo.h index 4961b0e..60cbc4f 100644 --- a/src/pipewire/nodeinfo.h +++ b/src/pipewire/nodeinfo.h @@ -25,13 +25,14 @@ enum class MediaClass { struct PortInfo { uint32_t id; + uint32_t nodeId; QString name; uint32_t direction; QString channelName; - PortInfo() : id(0), direction(0) {} - PortInfo(uint32_t portId, const QString &portName, uint32_t portDir, const QString &channel = QString()) - : id(portId), name(portName), direction(portDir), channelName(channel) {} + PortInfo() : id(0), nodeId(0), direction(0) {} + PortInfo(uint32_t portId, uint32_t owningNodeId, const QString &portName, uint32_t portDir, const QString &channel = QString()) + : id(portId), nodeId(owningNodeId), name(portName), direction(portDir), channelName(channel) {} }; struct NodeInfo { diff --git a/src/pipewire/pipewirecontroller.cpp b/src/pipewire/pipewirecontroller.cpp index 12d67c7..612c413 100644 --- a/src/pipewire/pipewirecontroller.cpp +++ b/src/pipewire/pipewirecontroller.cpp @@ -6,11 +6,15 @@ #include #include #include +#include #include #include #include +#include #include +#include +#include #include #include @@ -110,6 +114,51 @@ static const struct pw_core_events core_events = []() { return events; }(); +void meterProcess(void *data) +{ + auto *self = static_cast(data); + if (!self || !self->m_meterStream) { + return; + } + + struct pw_buffer *buf = pw_stream_dequeue_buffer(self->m_meterStream); + if (!buf || !buf->buffer || buf->buffer->n_datas == 0) { + if (buf) { + pw_stream_queue_buffer(self->m_meterStream, buf); + } + return; + } + + struct spa_buffer *spaBuf = buf->buffer; + struct spa_data *data0 = &spaBuf->datas[0]; + if (!data0->data || !data0->chunk) { + pw_stream_queue_buffer(self->m_meterStream, buf); + return; + } + + const uint32_t size = data0->chunk->size; + const float *samples = static_cast(data0->data); + const uint32_t count = size / sizeof(float); + + float peak = 0.0f; + for (uint32_t i = 0; i < count; ++i) { + const float value = std::fabs(samples[i]); + if (value > peak) { + peak = value; + } + } + + self->m_meterPeak.store(peak, std::memory_order_relaxed); + pw_stream_queue_buffer(self->m_meterStream, buf); +} + +static const struct pw_stream_events meter_events = []() { + struct pw_stream_events events{}; + events.version = PW_VERSION_STREAM_EVENTS; + events.process = meterProcess; + return events; +}(); + PipeWireController::PipeWireController(QObject *parent) : QObject(parent) { @@ -169,6 +218,10 @@ bool PipeWireController::initialize() } pw_registry_add_listener(m_registry, m_registryListener, ®istry_events, this); + + if (!setupMeterStream()) { + qWarning() << "Failed to set up meter stream"; + } unlock(); @@ -201,6 +254,8 @@ void PipeWireController::shutdown() pw_proxy_destroy(reinterpret_cast(m_registry)); m_registry = nullptr; } + + teardownMeterStream(); if (m_core) { pw_core_disconnect(m_core); @@ -250,6 +305,11 @@ QVector PipeWireController::links() const return m_links.values().toVector(); } +float PipeWireController::meterPeak() const +{ + return m_meterPeak.load(std::memory_order_relaxed); +} + uint32_t PipeWireController::createLink(uint32_t outputNodeId, uint32_t outputPortId, uint32_t inputNodeId, uint32_t inputPortId) { @@ -421,6 +481,22 @@ void PipeWireController::handleNodeInfo(uint32_t id, const struct spa_dict *prop node.mediaClass = NodeInfo::mediaClassFromString(mediaClassStr); node.type = NodeInfo::typeFromProperties(mediaClassStr, appNameStr); + + { + QMutexLocker lock(&m_nodesMutex); + for (auto it = m_ports.cbegin(); it != m_ports.cend(); ++it) { + const PortInfo &port = it.value(); + if (port.nodeId != id) { + continue; + } + + if (port.direction == PW_DIRECTION_INPUT) { + node.inputPorts.append(port); + } else if (port.direction == PW_DIRECTION_OUTPUT) { + node.outputPorts.append(port); + } + } + } { QMutexLocker lock(&m_nodesMutex); @@ -460,22 +536,23 @@ void PipeWireController::handlePortInfo(uint32_t id, const struct spa_dict *prop QString portName = name ? toQString(name) : QString("port_") + QString::number(id); - PortInfo port(id, portName, direction); + uint32_t nodeId = nodeIdStr ? static_cast(atoi(nodeIdStr)) : 0; + PortInfo port(id, nodeId, portName, direction); { QMutexLocker lock(&m_nodesMutex); m_ports.insert(id, port); - if (nodeIdStr) { - uint32_t nodeId = static_cast(atoi(nodeIdStr)); - if (m_nodes.contains(nodeId)) { - NodeInfo &node = m_nodes[nodeId]; - if (direction == PW_DIRECTION_INPUT) { - node.inputPorts.append(port); - } else if (direction == PW_DIRECTION_OUTPUT) { - node.outputPorts.append(port); + if (nodeId != 0 && m_nodes.contains(nodeId)) { + NodeInfo &node = m_nodes[nodeId]; + auto &ports = (direction == PW_DIRECTION_INPUT) ? node.inputPorts : node.outputPorts; + for (int i = 0; i < ports.size(); ++i) { + if (ports.at(i).id == id) { + ports.removeAt(i); + break; } } + ports.append(port); } } @@ -525,4 +602,62 @@ void PipeWireController::unlock() } } +bool PipeWireController::setupMeterStream() +{ + if (!m_threadLoop || !m_core) { + return false; + } + + struct pw_properties *props = pw_properties_new( + PW_KEY_MEDIA_TYPE, "Audio", + PW_KEY_MEDIA_CATEGORY, "Capture", + PW_KEY_MEDIA_CLASS, "Audio/Source", + nullptr); + + m_meterStream = pw_stream_new_simple( + pw_thread_loop_get_loop(m_threadLoop), + "Potato-Meter", + props, + &meter_events, + this); + + if (!m_meterStream) { + pw_properties_free(props); + return false; + } + + 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 struct spa_pod *params[1]; + params[0] = spa_format_audio_raw_build(&builder, SPA_PARAM_EnumFormat, &info); + + const int res = pw_stream_connect( + m_meterStream, + PW_DIRECTION_INPUT, + PW_ID_ANY, + static_cast(PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS | PW_STREAM_FLAG_RT_PROCESS), + params, + 1); + + return res == 0; +} + +void PipeWireController::teardownMeterStream() +{ + if (!m_meterStream) { + return; + } + + pw_stream_destroy(m_meterStream); + m_meterStream = nullptr; +} + } // namespace Potato diff --git a/src/pipewire/pipewirecontroller.h b/src/pipewire/pipewirecontroller.h index c2240fa..68e93d7 100644 --- a/src/pipewire/pipewirecontroller.h +++ b/src/pipewire/pipewirecontroller.h @@ -5,11 +5,13 @@ #include #include #include +#include struct pw_thread_loop; struct pw_context; struct pw_core; struct pw_registry; +struct pw_stream; struct spa_hook; struct spa_dict; @@ -31,6 +33,7 @@ public: QVector nodes() const; NodeInfo nodeById(uint32_t id) const; QVector links() const; + float meterPeak() const; uint32_t createLink(uint32_t outputNodeId, uint32_t outputPortId, uint32_t inputNodeId, uint32_t inputPortId); @@ -58,10 +61,13 @@ private: friend void registryEventGlobalRemove(void *data, uint32_t id); friend void coreEventDone(void *data, uint32_t id, int seq); friend void coreEventError(void *data, uint32_t id, int seq, int res, const char *message); + friend void meterProcess(void *data); void handleNodeInfo(uint32_t id, const struct ::spa_dict *props); void handlePortInfo(uint32_t id, const struct ::spa_dict *props); void handleLinkInfo(uint32_t id, const struct ::spa_dict *props); + bool setupMeterStream(); + void teardownMeterStream(); void lock(); void unlock(); @@ -70,6 +76,7 @@ private: pw_context *m_context = nullptr; pw_core *m_core = nullptr; pw_registry *m_registry = nullptr; + pw_stream *m_meterStream = nullptr; spa_hook *m_registryListener = nullptr; spa_hook *m_coreListener = nullptr; @@ -81,6 +88,7 @@ private: QAtomicInteger m_connected{false}; QAtomicInteger m_initialized{false}; + std::atomic m_meterPeak{0.0f}; uint32_t m_nodeIdCounter = 0; uint32_t m_linkIdCounter = 0;