Nodes
This commit is contained in:
parent
4addf989cc
commit
87e5aca9d8
11 changed files with 459 additions and 33 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 ¢er) const
|
|||
center = m_viewCenter;
|
||||
return true;
|
||||
}
|
||||
#include <QtNodes/StyleCollection>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
65
src/meters/AudioLevelMeter.cpp
Normal file
65
src/meters/AudioLevelMeter.cpp
Normal 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));
|
||||
}
|
||||
24
src/meters/AudioLevelMeter.h
Normal file
24
src/meters/AudioLevelMeter.h
Normal 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;
|
||||
};
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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, ®istry_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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue