Virtual
This commit is contained in:
parent
96e1a5cbdb
commit
ecfb59501a
5 changed files with 146 additions and 4 deletions
|
|
@ -1215,12 +1215,12 @@ private:
|
|||
- [x] Create 30Hz update timer with manual viewport control
|
||||
- [x] Integrate PipeWire audio callbacks for meter data
|
||||
- [x] Implement lock-free meter data transfer (atomics)
|
||||
- [ ] Profile and optimize rendering performance
|
||||
- [x] Profile and optimize rendering performance
|
||||
- [ ] **Acceptance Criteria:** Smooth 30Hz meters with no GUI lag, validated with profiler
|
||||
|
||||
### Milestone 4: Virtual Devices & State Management
|
||||
**Estimated Time:** 2 weeks
|
||||
- [ ] Implement virtual sink/source creation via PipeWire adapters
|
||||
- [x] Implement virtual sink/source creation via PipeWire adapters
|
||||
- [ ] Create `PresetManager` with JSON serialization
|
||||
- [ ] Implement preset load/save functionality
|
||||
- [ ] Store UI layout alongside audio graph state
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
#include <QAction>
|
||||
#include <QCoreApplication>
|
||||
#include <QDebug>
|
||||
#include <QColor>
|
||||
#include <QFileDialog>
|
||||
#include <QFontMetrics>
|
||||
|
|
@ -14,6 +15,8 @@
|
|||
#include <QVBoxLayout>
|
||||
#include <QScrollArea>
|
||||
#include <QSizePolicy>
|
||||
#include <QElapsedTimer>
|
||||
#include <algorithm>
|
||||
|
||||
#include <QtNodes/GraphicsViewStyle>
|
||||
#include <QtNodes/NodeStyle>
|
||||
|
|
@ -172,6 +175,28 @@ GraphEditorWidget::GraphEditorWidget(Potato::PipeWireController *controller, QWi
|
|||
});
|
||||
m_view->addAction(resetLayoutAction);
|
||||
|
||||
auto *createVirtualSinkAction = new QAction(QString("Create Virtual Sink"), m_view);
|
||||
connect(createVirtualSinkAction, &QAction::triggered, [this]() {
|
||||
const int index = ++m_virtualSinkCount;
|
||||
const QString name = QString("Potato_Virtual_Sink_%1").arg(index);
|
||||
const QString description = QString("Virtual Sink %1").arg(index);
|
||||
if (!m_controller->createVirtualSink(name, description, 2, 48000)) {
|
||||
qWarning() << "Failed to create virtual sink" << name;
|
||||
}
|
||||
});
|
||||
m_view->addAction(createVirtualSinkAction);
|
||||
|
||||
auto *createVirtualSourceAction = new QAction(QString("Create Virtual Source"), m_view);
|
||||
connect(createVirtualSourceAction, &QAction::triggered, [this]() {
|
||||
const int index = ++m_virtualSourceCount;
|
||||
const QString name = QString("Potato_Virtual_Source_%1").arg(index);
|
||||
const QString description = QString("Virtual Source %1").arg(index);
|
||||
if (!m_controller->createVirtualSource(name, description, 2, 48000)) {
|
||||
qWarning() << "Failed to create virtual source" << name;
|
||||
}
|
||||
});
|
||||
m_view->addAction(createVirtualSourceAction);
|
||||
|
||||
syncGraph();
|
||||
if (m_model->hasOverlaps()) {
|
||||
m_model->autoArrange();
|
||||
|
|
@ -205,6 +230,9 @@ GraphEditorWidget::GraphEditorWidget(Potato::PipeWireController *controller, QWi
|
|||
m_meterTimer->setTimerType(Qt::PreciseTimer);
|
||||
connect(m_meterTimer, &QTimer::timeout, this, &GraphEditorWidget::updateMeter);
|
||||
m_meterTimer->start();
|
||||
|
||||
m_meterProfileTimer.start();
|
||||
m_meterProfileReady = true;
|
||||
}
|
||||
|
||||
static bool isAudioEndpoint(const Potato::NodeInfo &node)
|
||||
|
|
@ -383,6 +411,7 @@ void GraphEditorWidget::onConnectionCreated(QtNodes::ConnectionId const connecti
|
|||
|
||||
const uint32_t linkId = m_controller->createLink(outInfo->id, outputPortId, inInfo->id, inputPortId);
|
||||
if (linkId == 0) {
|
||||
m_model->deleteConnection(connectionId);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -435,6 +464,12 @@ void GraphEditorWidget::updateMeter()
|
|||
return;
|
||||
}
|
||||
|
||||
if (!isVisible()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const qint64 startNanos = m_meterProfileReady ? m_meterProfileTimer.nsecsElapsed() : 0;
|
||||
|
||||
const float peak = m_controller->meterPeak();
|
||||
m_meter->setLevel(peak);
|
||||
|
||||
|
|
@ -443,6 +478,21 @@ void GraphEditorWidget::updateMeter()
|
|||
const float nodePeak = m_controller->nodeMeterPeak(nodeId);
|
||||
it.value()->setLevel(nodePeak);
|
||||
}
|
||||
|
||||
if (m_meterProfileReady) {
|
||||
const qint64 duration = m_meterProfileTimer.nsecsElapsed() - startNanos;
|
||||
m_meterProfileNanos += duration;
|
||||
m_meterProfileMax = std::max(m_meterProfileMax, duration);
|
||||
++m_meterProfileFrames;
|
||||
if (m_meterProfileFrames >= 300) {
|
||||
const double avgMs = static_cast<double>(m_meterProfileNanos) / (1000000.0 * m_meterProfileFrames);
|
||||
const double maxMs = static_cast<double>(m_meterProfileMax) / 1000000.0;
|
||||
qInfo() << "Meter update avg" << avgMs << "ms max" << maxMs << "ms";
|
||||
m_meterProfileNanos = 0;
|
||||
m_meterProfileMax = 0;
|
||||
m_meterProfileFrames = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GraphEditorWidget::updateLayoutState()
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
#include <QWidget>
|
||||
#include <QSet>
|
||||
#include <QMap>
|
||||
#include <QElapsedTimer>
|
||||
#include <cstdint>
|
||||
|
||||
class AudioLevelMeter;
|
||||
|
|
@ -59,6 +60,8 @@ private:
|
|||
QSet<uint32_t> m_ignoreLinkRemoved;
|
||||
QMap<QString, uint32_t> m_connectionToLinkId;
|
||||
QMap<uint32_t, QString> m_linkIdToConnection;
|
||||
int m_virtualSinkCount = 0;
|
||||
int m_virtualSourceCount = 0;
|
||||
AudioLevelMeter *m_meter = nullptr;
|
||||
QTimer *m_meterTimer = nullptr;
|
||||
QScrollArea *m_meterScroll = nullptr;
|
||||
|
|
@ -69,4 +72,9 @@ private:
|
|||
QMap<uint32_t, QLabel*> m_nodeMeterLabels;
|
||||
QMap<uint32_t, int> m_nodeLinkCounts;
|
||||
QMap<uint32_t, Potato::LinkInfo> m_linksById;
|
||||
QElapsedTimer m_meterProfileTimer;
|
||||
bool m_meterProfileReady = false;
|
||||
qint64 m_meterProfileNanos = 0;
|
||||
qint64 m_meterProfileMax = 0;
|
||||
int m_meterProfileFrames = 0;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
#include <spa/param/audio/format-utils.h>
|
||||
#include <spa/param/audio/raw.h>
|
||||
#include <spa/utils/dict.h>
|
||||
#include <spa/utils/defs.h>
|
||||
#include <spa/utils/type-info.h>
|
||||
|
||||
namespace Potato {
|
||||
|
|
@ -76,6 +77,56 @@ static bool readRingLatest(spa_ringbuffer *ring, std::vector<uint8_t> &buffer, f
|
|||
return true;
|
||||
}
|
||||
|
||||
bool PipeWireController::createVirtualDevice(const QString &name,
|
||||
const QString &description,
|
||||
const char *factoryName,
|
||||
const char *mediaClass,
|
||||
int channels,
|
||||
int rate)
|
||||
{
|
||||
if (!m_threadLoop || !m_core || name.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
channels = channels > 0 ? channels : 2;
|
||||
rate = rate > 0 ? rate : 48000;
|
||||
|
||||
const QByteArray nameBytes = name.toUtf8();
|
||||
const QByteArray descBytes = description.isEmpty() ? nameBytes : description.toUtf8();
|
||||
const QByteArray channelsBytes = QByteArray::number(channels);
|
||||
const QByteArray rateBytes = QByteArray::number(rate);
|
||||
|
||||
struct spa_dict_item items[] = {
|
||||
{ PW_KEY_FACTORY_NAME, factoryName },
|
||||
{ PW_KEY_NODE_NAME, nameBytes.constData() },
|
||||
{ PW_KEY_NODE_DESCRIPTION, descBytes.constData() },
|
||||
{ PW_KEY_MEDIA_CLASS, mediaClass },
|
||||
{ PW_KEY_AUDIO_CHANNELS, channelsBytes.constData() },
|
||||
{ PW_KEY_AUDIO_RATE, rateBytes.constData() },
|
||||
{ "object.linger", "true" },
|
||||
{ PW_KEY_APP_NAME, "Potato-Manager" }
|
||||
};
|
||||
|
||||
struct spa_dict dict = SPA_DICT_INIT(items, SPA_N_ELEMENTS(items));
|
||||
|
||||
lock();
|
||||
auto *proxy = static_cast<struct pw_proxy*>(pw_core_create_object(
|
||||
m_core,
|
||||
"adapter",
|
||||
PW_TYPE_INTERFACE_Node,
|
||||
PW_VERSION_NODE,
|
||||
&dict,
|
||||
0));
|
||||
unlock();
|
||||
|
||||
if (!proxy) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_virtualDevices.push_back(proxy);
|
||||
return true;
|
||||
}
|
||||
|
||||
static QString toQString(const char *value)
|
||||
{
|
||||
if (!value) {
|
||||
|
|
@ -383,6 +434,13 @@ void PipeWireController::shutdown()
|
|||
m_nodeMeters.clear();
|
||||
}
|
||||
|
||||
for (auto *proxy : m_virtualDevices) {
|
||||
if (proxy) {
|
||||
pw_proxy_destroy(proxy);
|
||||
}
|
||||
}
|
||||
m_virtualDevices.clear();
|
||||
|
||||
if (m_core) {
|
||||
pw_core_disconnect(m_core);
|
||||
m_core = nullptr;
|
||||
|
|
@ -480,6 +538,16 @@ bool PipeWireController::setNodeVolume(uint32_t nodeId, float volume, bool mute)
|
|||
return true;
|
||||
}
|
||||
|
||||
bool PipeWireController::createVirtualSink(const QString &name, const QString &description, int channels, int rate)
|
||||
{
|
||||
return createVirtualDevice(name, description, "support.null-audio-sink", "Audio/Sink", channels, rate);
|
||||
}
|
||||
|
||||
bool PipeWireController::createVirtualSource(const QString &name, const QString &description, int channels, int rate)
|
||||
{
|
||||
return createVirtualDevice(name, description, "support.null-audio-source", "Audio/Source", channels, rate);
|
||||
}
|
||||
|
||||
float PipeWireController::nodeMeterPeak(uint32_t nodeId) const
|
||||
{
|
||||
QMutexLocker lock(&m_meterMutex);
|
||||
|
|
@ -829,6 +897,8 @@ void PipeWireController::handlePortInfo(uint32_t id, const struct spa_dict *prop
|
|||
uint32_t nodeId = nodeIdStr ? static_cast<uint32_t>(atoi(nodeIdStr)) : 0;
|
||||
PortInfo port(id, nodeId, portName, direction);
|
||||
|
||||
bool emitChanged = false;
|
||||
NodeInfo nodeSnapshot;
|
||||
{
|
||||
QMutexLocker lock(&m_nodesMutex);
|
||||
m_ports.insert(id, port);
|
||||
|
|
@ -843,9 +913,15 @@ void PipeWireController::handlePortInfo(uint32_t id, const struct spa_dict *prop
|
|||
}
|
||||
}
|
||||
ports.append(port);
|
||||
nodeSnapshot = node;
|
||||
emitChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (emitChanged) {
|
||||
emit nodeChanged(nodeSnapshot);
|
||||
}
|
||||
|
||||
qDebug() << "Port added:" << id << portName << "direction:" << direction;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ struct pw_context;
|
|||
struct pw_core;
|
||||
struct pw_registry;
|
||||
struct pw_stream;
|
||||
struct pw_proxy;
|
||||
struct spa_hook;
|
||||
struct spa_dict;
|
||||
namespace Potato {
|
||||
|
|
@ -42,6 +43,8 @@ public:
|
|||
void ensureNodeMeter(uint32_t nodeId, const QString &targetName, bool captureSink);
|
||||
void removeNodeMeter(uint32_t nodeId);
|
||||
bool setNodeVolume(uint32_t nodeId, float volume, bool mute);
|
||||
bool createVirtualSink(const QString &name, const QString &description, int channels, int rate);
|
||||
bool createVirtualSource(const QString &name, const QString &description, int channels, int rate);
|
||||
|
||||
uint32_t createLink(uint32_t outputNodeId, uint32_t outputPortId,
|
||||
uint32_t inputNodeId, uint32_t inputPortId);
|
||||
|
|
@ -76,6 +79,9 @@ private:
|
|||
void handleLinkInfo(uint32_t id, const struct ::spa_dict *props);
|
||||
bool setupMeterStream();
|
||||
void teardownMeterStream();
|
||||
bool createVirtualDevice(const QString &name, const QString &description,
|
||||
const char *factoryName, const char *mediaClass,
|
||||
int channels, int rate);
|
||||
|
||||
void lock();
|
||||
void unlock();
|
||||
|
|
@ -101,6 +107,8 @@ private:
|
|||
mutable std::vector<uint8_t> m_meterRingData;
|
||||
std::atomic<bool> m_meterRingReady{false};
|
||||
|
||||
std::vector<struct pw_proxy*> m_virtualDevices;
|
||||
|
||||
mutable QMutex m_meterMutex;
|
||||
QMap<uint32_t, NodeMeter*> m_nodeMeters;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue