This commit is contained in:
Joey Yakimowich-Payne 2026-01-27 16:41:51 -07:00
commit 87e5aca9d8
11 changed files with 459 additions and 33 deletions

View file

@ -89,6 +89,7 @@ add_executable(potato-gui
src/main_gui.cpp src/main_gui.cpp
src/gui/GraphEditorWidget.cpp src/gui/GraphEditorWidget.cpp
src/gui/PipeWireGraphModel.cpp src/gui/PipeWireGraphModel.cpp
src/meters/AudioLevelMeter.cpp
) )
target_link_libraries(potato-gui PRIVATE target_link_libraries(potato-gui PRIVATE

View file

@ -1,22 +1,84 @@
#include "GraphEditorWidget.h" #include "GraphEditorWidget.h"
#include "meters/AudioLevelMeter.h"
#include <QAction> #include <QAction>
#include <QCoreApplication> #include <QCoreApplication>
#include <QColor>
#include <QFileDialog> #include <QFileDialog>
#include <QLabel>
#include <QSplitter>
#include <QTimer>
#include <QVBoxLayout> #include <QVBoxLayout>
#include <QtNodes/GraphicsViewStyle>
#include <QtNodes/NodeStyle>
GraphEditorWidget::GraphEditorWidget(Potato::PipeWireController *controller, QWidget *parent) GraphEditorWidget::GraphEditorWidget(Potato::PipeWireController *controller, QWidget *parent)
: QWidget(parent) : QWidget(parent)
, m_controller(controller) , 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 = new PipeWireGraphModel(controller, this);
m_model->loadLayout(); m_model->loadLayout();
m_scene = new QtNodes::BasicGraphicsScene(*m_model, this); m_scene = new QtNodes::BasicGraphicsScene(*m_model, this);
m_view = new QtNodes::GraphicsView(m_scene); 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); auto *layout = new QVBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0); layout->setContentsMargins(0, 0, 0, 0);
layout->addWidget(m_view); layout->addWidget(splitter);
setLayout(layout); setLayout(layout);
connect(m_model, &PipeWireGraphModel::connectionCreated, connect(m_model, &PipeWireGraphModel::connectionCreated,
@ -26,6 +88,8 @@ GraphEditorWidget::GraphEditorWidget(Potato::PipeWireController *controller, QWi
connect(m_controller, &Potato::PipeWireController::nodeAdded, connect(m_controller, &Potato::PipeWireController::nodeAdded,
this, &GraphEditorWidget::onNodeAdded); this, &GraphEditorWidget::onNodeAdded);
connect(m_controller, &Potato::PipeWireController::nodeChanged,
this, &GraphEditorWidget::onNodeChanged);
connect(m_controller, &Potato::PipeWireController::nodeRemoved, connect(m_controller, &Potato::PipeWireController::nodeRemoved,
this, &GraphEditorWidget::onNodeRemoved); this, &GraphEditorWidget::onNodeRemoved);
connect(m_controller, &Potato::PipeWireController::linkAdded, connect(m_controller, &Potato::PipeWireController::linkAdded,
@ -92,13 +156,28 @@ GraphEditorWidget::GraphEditorWidget(Potato::PipeWireController *controller, QWi
m_model->saveLayout(); 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() void GraphEditorWidget::syncGraph()
{ {
const QVector<Potato::NodeInfo> nodes = m_controller->nodes(); const QVector<Potato::NodeInfo> nodes = m_controller->nodes();
for (const auto &node : nodes) { for (const auto &node : nodes) {
m_model->addPipeWireNode(node); if (isAudioEndpoint(node)) {
m_model->addPipeWireNode(node);
}
} }
const QVector<Potato::LinkInfo> links = m_controller->links(); const QVector<Potato::LinkInfo> links = m_controller->links();
@ -124,7 +203,20 @@ void GraphEditorWidget::refreshGraph()
void GraphEditorWidget::onNodeAdded(const Potato::NodeInfo &node) 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) 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.inNodeId)
+ QString(":") + QString::number(connectionId.inPortIndex); + QString(":") + QString::number(connectionId.inPortIndex);
} }
void GraphEditorWidget::updateMeter()
{
if (!m_meter) {
return;
}
const float peak = m_controller->meterPeak();
m_meter->setLevel(peak);
}

View file

@ -11,6 +11,9 @@
#include <QMap> #include <QMap>
#include <cstdint> #include <cstdint>
class AudioLevelMeter;
class QTimer;
class GraphEditorWidget : public QWidget class GraphEditorWidget : public QWidget
{ {
Q_OBJECT Q_OBJECT
@ -20,11 +23,13 @@ public:
private slots: private slots:
void onNodeAdded(const Potato::NodeInfo &node); void onNodeAdded(const Potato::NodeInfo &node);
void onNodeChanged(const Potato::NodeInfo &node);
void onNodeRemoved(uint32_t nodeId); void onNodeRemoved(uint32_t nodeId);
void onLinkAdded(const Potato::LinkInfo &link); void onLinkAdded(const Potato::LinkInfo &link);
void onLinkRemoved(uint32_t linkId); void onLinkRemoved(uint32_t linkId);
void onConnectionCreated(QtNodes::ConnectionId const connectionId); void onConnectionCreated(QtNodes::ConnectionId const connectionId);
void onConnectionDeleted(QtNodes::ConnectionId const connectionId); void onConnectionDeleted(QtNodes::ConnectionId const connectionId);
void updateMeter();
private: private:
void syncGraph(); void syncGraph();
@ -40,4 +45,6 @@ private:
QSet<QString> m_ignoreDelete; QSet<QString> m_ignoreDelete;
QMap<QString, uint32_t> m_connectionToLinkId; QMap<QString, uint32_t> m_connectionToLinkId;
QMap<uint32_t, QString> m_linkIdToConnection; QMap<uint32_t, QString> m_linkIdToConnection;
AudioLevelMeter *m_meter = nullptr;
QTimer *m_meterTimer = nullptr;
}; };

View file

@ -1,19 +1,56 @@
#include "PipeWireGraphModel.h" #include "PipeWireGraphModel.h"
#include "PipeWireGraphModel.h"
#include <QtCore/QJsonArray> #include <QtCore/QJsonArray>
#include <QtCore/QJsonDocument> #include <QtCore/QJsonDocument>
#include <QtCore/QJsonObject> #include <QtCore/QJsonObject>
#include <QtCore/QObject> #include <QtCore/QObject>
#include <QtCore/QStandardPaths> #include <QtCore/QStandardPaths>
#include <QtGui/QFont>
#include <QtGui/QFontMetrics>
#include <QDir> #include <QDir>
#include <QFile> #include <QFile>
#include <QFileInfo> #include <QFileInfo>
#include <QtNodes/StyleCollection>
#include <algorithm> #include <algorithm>
#include <unordered_set> #include <unordered_set>
#include <vector> #include <vector>
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) PipeWireGraphModel::PipeWireGraphModel(Potato::PipeWireController *controller, QObject *parent)
: QtNodes::AbstractGraphModel() : QtNodes::AbstractGraphModel()
, m_controller(controller) , m_controller(controller)
@ -154,7 +191,12 @@ QVariant PipeWireGraphModel::nodeData(QtNodes::NodeId nodeId, QtNodes::NodeRole
switch (role) { switch (role) {
case QtNodes::NodeRole::Caption: 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: case QtNodes::NodeRole::CaptionVisible:
return true; return true;
case QtNodes::NodeRole::Position: { case QtNodes::NodeRole::Position: {
@ -165,13 +207,24 @@ QVariant PipeWireGraphModel::nodeData(QtNodes::NodeId nodeId, QtNodes::NodeRole
return QPointF(0, 0); return QPointF(0, 0);
} }
case QtNodes::NodeRole::Size: 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: case QtNodes::NodeRole::InPortCount:
return static_cast<unsigned int>(info.inputPorts.size()); return static_cast<unsigned int>(info.inputPorts.size());
case QtNodes::NodeRole::OutPortCount: case QtNodes::NodeRole::OutPortCount:
return static_cast<unsigned int>(info.outputPorts.size()); return static_cast<unsigned int>(info.outputPorts.size());
case QtNodes::NodeRole::Type: case QtNodes::NodeRole::Type:
return QString("PipeWire"); return QString("PipeWire");
case QtNodes::NodeRole::Style:
return QtNodes::StyleCollection::nodeStyle().toJson().toVariantMap();
default: default:
return QVariant(); return QVariant();
} }
@ -217,11 +270,15 @@ QVariant PipeWireGraphModel::portData(QtNodes::NodeId nodeId,
if (role == QtNodes::PortRole::Caption) { if (role == QtNodes::PortRole::Caption) {
if (portType == QtNodes::PortType::In) { if (portType == QtNodes::PortType::In) {
if (portIndex < static_cast<QtNodes::PortIndex>(info.inputPorts.size())) { if (portIndex < static_cast<QtNodes::PortIndex>(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) { } else if (portType == QtNodes::PortType::Out) {
if (portIndex < static_cast<QtNodes::PortIndex>(info.outputPorts.size())) { if (portIndex < static_cast<QtNodes::PortIndex>(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; 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 const Potato::NodeInfo *PipeWireGraphModel::nodeInfo(QtNodes::NodeId nodeId) const
{ {
auto it = m_nodes.find(nodeId); auto it = m_nodes.find(nodeId);
@ -650,3 +720,4 @@ bool PipeWireGraphModel::viewState(double &scale, QPointF &center) const
center = m_viewCenter; center = m_viewCenter;
return true; return true;
} }
#include <QtNodes/StyleCollection>

View file

@ -56,6 +56,7 @@ public:
bool findConnectionForLink(uint32_t linkId, QtNodes::ConnectionId &connectionId) const; bool findConnectionForLink(uint32_t linkId, QtNodes::ConnectionId &connectionId) const;
const Potato::NodeInfo *nodeInfo(QtNodes::NodeId nodeId) const; const Potato::NodeInfo *nodeInfo(QtNodes::NodeId nodeId) const;
bool connectionIdForLink(const Potato::LinkInfo &link, QtNodes::ConnectionId &connectionId) const; bool connectionIdForLink(const Potato::LinkInfo &link, QtNodes::ConnectionId &connectionId) const;
bool updatePipeWireNode(const Potato::NodeInfo &node);
void reset(); void reset();
void loadLayout(); void loadLayout();
void saveLayout() const; void saveLayout() const;

View file

@ -3,6 +3,7 @@
#include <QCommandLineParser> #include <QCommandLineParser>
#include <QDateTime> #include <QDateTime>
#include <QDir> #include <QDir>
#include <QFileInfo>
#include <QKeySequence> #include <QKeySequence>
#include <QMainWindow> #include <QMainWindow>
#include <QScreen> #include <QScreen>
@ -44,11 +45,6 @@ int main(int argc, char *argv[])
auto *screenshotAction = new QAction(&window); auto *screenshotAction = new QAction(&window);
screenshotAction->setShortcut(QKeySequence(Qt::Key_F12)); screenshotAction->setShortcut(QKeySequence(Qt::Key_F12));
QObject::connect(screenshotAction, &QAction::triggered, [&window]() { QObject::connect(screenshotAction, &QAction::triggered, [&window]() {
QScreen *screen = window.screen();
if (!screen) {
return;
}
const QString baseDir = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation) const QString baseDir = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation)
+ QString("/potato"); + QString("/potato");
QDir().mkpath(baseDir); QDir().mkpath(baseDir);
@ -57,8 +53,17 @@ int main(int argc, char *argv[])
.arg(QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss")); .arg(QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss"));
const QString filePath = baseDir + QString("/") + fileName; const QString filePath = baseDir + QString("/") + fileName;
const QPixmap pixmap = screen->grabWindow(window.winId()); QPixmap pixmap = window.grab();
pixmap.save(filePath, "PNG"); if (pixmap.isNull()) {
QScreen *screen = window.screen();
if (screen) {
pixmap = screen->grabWindow(window.winId());
}
}
if (!pixmap.isNull()) {
pixmap.save(filePath, "PNG");
}
}); });
window.addAction(screenshotAction); window.addAction(screenshotAction);
@ -71,15 +76,21 @@ int main(int argc, char *argv[])
const QString screenshotPath = parser.value(screenshotOption); const QString screenshotPath = parser.value(screenshotOption);
if (!screenshotPath.isEmpty()) { if (!screenshotPath.isEmpty()) {
QTimer::singleShot(500, &window, [&window, &parser, screenshotPath]() { QTimer::singleShot(800, &window, [&window, &parser, screenshotPath]() {
QScreen *screen = window.screen(); const QFileInfo info(screenshotPath);
if (!screen) { if (!info.absolutePath().isEmpty()) {
QCoreApplication::exit(2); QDir().mkpath(info.absolutePath());
return;
} }
const QPixmap pixmap = screen->grabWindow(window.winId()); QPixmap pixmap = window.grab();
if (!pixmap.save(screenshotPath)) { if (pixmap.isNull()) {
QScreen *screen = window.screen();
if (screen) {
pixmap = screen->grabWindow(window.winId());
}
}
if (pixmap.isNull() || !pixmap.save(screenshotPath)) {
QCoreApplication::exit(3); QCoreApplication::exit(3);
return; return;
} }

View file

@ -0,0 +1,65 @@
#include "AudioLevelMeter.h"
#include <QPainter>
#include <QtGlobal>
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));
}

View file

@ -0,0 +1,24 @@
#pragma once
#include <QWidget>
#include <QSize>
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;
};

View file

@ -25,13 +25,14 @@ enum class MediaClass {
struct PortInfo { struct PortInfo {
uint32_t id; uint32_t id;
uint32_t nodeId;
QString name; QString name;
uint32_t direction; uint32_t direction;
QString channelName; QString channelName;
PortInfo() : id(0), direction(0) {} PortInfo() : id(0), nodeId(0), direction(0) {}
PortInfo(uint32_t portId, const QString &portName, uint32_t portDir, const QString &channel = QString()) PortInfo(uint32_t portId, uint32_t owningNodeId, const QString &portName, uint32_t portDir, const QString &channel = QString())
: id(portId), name(portName), direction(portDir), channelName(channel) {} : id(portId), nodeId(owningNodeId), name(portName), direction(portDir), channelName(channel) {}
}; };
struct NodeInfo { struct NodeInfo {

View file

@ -6,11 +6,15 @@
#include <QThread> #include <QThread>
#include <cstring> #include <cstring>
#include <cstdlib> #include <cstdlib>
#include <cmath>
#include <pipewire/pipewire.h> #include <pipewire/pipewire.h>
#include <pipewire/keys.h> #include <pipewire/keys.h>
#include <pipewire/properties.h> #include <pipewire/properties.h>
#include <pipewire/stream.h>
#include <spa/param/props.h> #include <spa/param/props.h>
#include <spa/param/audio/format-utils.h>
#include <spa/param/audio/raw.h>
#include <spa/utils/dict.h> #include <spa/utils/dict.h>
#include <spa/utils/type-info.h> #include <spa/utils/type-info.h>
@ -110,6 +114,51 @@ static const struct pw_core_events core_events = []() {
return events; return events;
}(); }();
void meterProcess(void *data)
{
auto *self = static_cast<PipeWireController*>(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<const float*>(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) PipeWireController::PipeWireController(QObject *parent)
: QObject(parent) : QObject(parent)
{ {
@ -169,6 +218,10 @@ bool PipeWireController::initialize()
} }
pw_registry_add_listener(m_registry, m_registryListener, &registry_events, this); pw_registry_add_listener(m_registry, m_registryListener, &registry_events, this);
if (!setupMeterStream()) {
qWarning() << "Failed to set up meter stream";
}
unlock(); unlock();
@ -201,6 +254,8 @@ void PipeWireController::shutdown()
pw_proxy_destroy(reinterpret_cast<struct pw_proxy*>(m_registry)); pw_proxy_destroy(reinterpret_cast<struct pw_proxy*>(m_registry));
m_registry = nullptr; m_registry = nullptr;
} }
teardownMeterStream();
if (m_core) { if (m_core) {
pw_core_disconnect(m_core); pw_core_disconnect(m_core);
@ -250,6 +305,11 @@ QVector<LinkInfo> PipeWireController::links() const
return m_links.values().toVector(); 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 PipeWireController::createLink(uint32_t outputNodeId, uint32_t outputPortId,
uint32_t inputNodeId, uint32_t inputPortId) 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.mediaClass = NodeInfo::mediaClassFromString(mediaClassStr);
node.type = NodeInfo::typeFromProperties(mediaClassStr, appNameStr); 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); 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 portName = name ? toQString(name)
: QString("port_") + QString::number(id); : QString("port_") + QString::number(id);
PortInfo port(id, portName, direction); uint32_t nodeId = nodeIdStr ? static_cast<uint32_t>(atoi(nodeIdStr)) : 0;
PortInfo port(id, nodeId, portName, direction);
{ {
QMutexLocker lock(&m_nodesMutex); QMutexLocker lock(&m_nodesMutex);
m_ports.insert(id, port); m_ports.insert(id, port);
if (nodeIdStr) { if (nodeId != 0 && m_nodes.contains(nodeId)) {
uint32_t nodeId = static_cast<uint32_t>(atoi(nodeIdStr)); NodeInfo &node = m_nodes[nodeId];
if (m_nodes.contains(nodeId)) { auto &ports = (direction == PW_DIRECTION_INPUT) ? node.inputPorts : node.outputPorts;
NodeInfo &node = m_nodes[nodeId]; for (int i = 0; i < ports.size(); ++i) {
if (direction == PW_DIRECTION_INPUT) { if (ports.at(i).id == id) {
node.inputPorts.append(port); ports.removeAt(i);
} else if (direction == PW_DIRECTION_OUTPUT) { break;
node.outputPorts.append(port);
} }
} }
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_flags>(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 } // namespace Potato

View file

@ -5,11 +5,13 @@
#include <QMap> #include <QMap>
#include <QMutex> #include <QMutex>
#include <QAtomicInteger> #include <QAtomicInteger>
#include <atomic>
struct pw_thread_loop; struct pw_thread_loop;
struct pw_context; struct pw_context;
struct pw_core; struct pw_core;
struct pw_registry; struct pw_registry;
struct pw_stream;
struct spa_hook; struct spa_hook;
struct spa_dict; struct spa_dict;
@ -31,6 +33,7 @@ public:
QVector<NodeInfo> nodes() const; QVector<NodeInfo> nodes() const;
NodeInfo nodeById(uint32_t id) const; NodeInfo nodeById(uint32_t id) const;
QVector<LinkInfo> links() const; QVector<LinkInfo> links() const;
float meterPeak() const;
uint32_t createLink(uint32_t outputNodeId, uint32_t outputPortId, uint32_t createLink(uint32_t outputNodeId, uint32_t outputPortId,
uint32_t inputNodeId, uint32_t inputPortId); uint32_t inputNodeId, uint32_t inputPortId);
@ -58,10 +61,13 @@ private:
friend void registryEventGlobalRemove(void *data, uint32_t id); friend void registryEventGlobalRemove(void *data, uint32_t id);
friend void coreEventDone(void *data, uint32_t id, int seq); 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 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 handleNodeInfo(uint32_t id, const struct ::spa_dict *props);
void handlePortInfo(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); void handleLinkInfo(uint32_t id, const struct ::spa_dict *props);
bool setupMeterStream();
void teardownMeterStream();
void lock(); void lock();
void unlock(); void unlock();
@ -70,6 +76,7 @@ private:
pw_context *m_context = nullptr; pw_context *m_context = nullptr;
pw_core *m_core = nullptr; pw_core *m_core = nullptr;
pw_registry *m_registry = nullptr; pw_registry *m_registry = nullptr;
pw_stream *m_meterStream = nullptr;
spa_hook *m_registryListener = nullptr; spa_hook *m_registryListener = nullptr;
spa_hook *m_coreListener = nullptr; spa_hook *m_coreListener = nullptr;
@ -81,6 +88,7 @@ private:
QAtomicInteger<bool> m_connected{false}; QAtomicInteger<bool> m_connected{false};
QAtomicInteger<bool> m_initialized{false}; QAtomicInteger<bool> m_initialized{false};
std::atomic<float> m_meterPeak{0.0f};
uint32_t m_nodeIdCounter = 0; uint32_t m_nodeIdCounter = 0;
uint32_t m_linkIdCounter = 0; uint32_t m_linkIdCounter = 0;