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/gui/GraphEditorWidget.cpp
src/gui/PipeWireGraphModel.cpp
src/meters/AudioLevelMeter.cpp
)
target_link_libraries(potato-gui PRIVATE

View file

@ -1,22 +1,84 @@
#include "GraphEditorWidget.h"
#include "meters/AudioLevelMeter.h"
#include <QAction>
#include <QCoreApplication>
#include <QColor>
#include <QFileDialog>
#include <QLabel>
#include <QSplitter>
#include <QTimer>
#include <QVBoxLayout>
#include <QtNodes/GraphicsViewStyle>
#include <QtNodes/NodeStyle>
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<Potato::NodeInfo> nodes = m_controller->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();
@ -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);
}

View file

@ -11,6 +11,9 @@
#include <QMap>
#include <cstdint>
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<QString> m_ignoreDelete;
QMap<QString, uint32_t> m_connectionToLinkId;
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 <QtCore/QJsonArray>
#include <QtCore/QJsonDocument>
#include <QtCore/QJsonObject>
#include <QtCore/QObject>
#include <QtCore/QStandardPaths>
#include <QtGui/QFont>
#include <QtGui/QFontMetrics>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QtNodes/StyleCollection>
#include <algorithm>
#include <unordered_set>
#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)
: 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<unsigned int>(info.inputPorts.size());
case QtNodes::NodeRole::OutPortCount:
return static_cast<unsigned int>(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<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) {
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;
}
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 &center) const
center = m_viewCenter;
return true;
}
#include <QtNodes/StyleCollection>

View file

@ -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;

View file

@ -3,6 +3,7 @@
#include <QCommandLineParser>
#include <QDateTime>
#include <QDir>
#include <QFileInfo>
#include <QKeySequence>
#include <QMainWindow>
#include <QScreen>
@ -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;
}

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 {
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 {

View file

@ -6,11 +6,15 @@
#include <QThread>
#include <cstring>
#include <cstdlib>
#include <cmath>
#include <pipewire/pipewire.h>
#include <pipewire/keys.h>
#include <pipewire/properties.h>
#include <pipewire/stream.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/type-info.h>
@ -110,6 +114,51 @@ static const struct pw_core_events core_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)
: QObject(parent)
{
@ -170,6 +219,10 @@ bool PipeWireController::initialize()
pw_registry_add_listener(m_registry, m_registryListener, &registry_events, this);
if (!setupMeterStream()) {
qWarning() << "Failed to set up meter stream";
}
unlock();
if (pw_thread_loop_start(m_threadLoop) < 0) {
@ -202,6 +255,8 @@ void PipeWireController::shutdown()
m_registry = nullptr;
}
teardownMeterStream();
if (m_core) {
pw_core_disconnect(m_core);
m_core = nullptr;
@ -250,6 +305,11 @@ QVector<LinkInfo> 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)
{
@ -422,6 +482,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<uint32_t>(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<uint32_t>(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_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

View file

@ -5,11 +5,13 @@
#include <QMap>
#include <QMutex>
#include <QAtomicInteger>
#include <atomic>
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<NodeInfo> nodes() const;
NodeInfo nodeById(uint32_t id) const;
QVector<LinkInfo> 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<bool> m_connected{false};
QAtomicInteger<bool> m_initialized{false};
std::atomic<float> m_meterPeak{0.0f};
uint32_t m_nodeIdCounter = 0;
uint32_t m_linkIdCounter = 0;