Volume
This commit is contained in:
parent
94ce8617d6
commit
4bf830763f
4 changed files with 285 additions and 3 deletions
|
|
@ -5,10 +5,13 @@
|
|||
#include <QCoreApplication>
|
||||
#include <QColor>
|
||||
#include <QFileDialog>
|
||||
#include <QFontMetrics>
|
||||
#include <QLabel>
|
||||
#include <QHBoxLayout>
|
||||
#include <QSplitter>
|
||||
#include <QTimer>
|
||||
#include <QVBoxLayout>
|
||||
#include <QScrollArea>
|
||||
|
||||
#include <QtNodes/GraphicsViewStyle>
|
||||
#include <QtNodes/NodeStyle>
|
||||
|
|
@ -74,9 +77,28 @@ GraphEditorWidget::GraphEditorWidget(Potato::PipeWireController *controller, QWi
|
|||
|
||||
m_meter = new AudioLevelMeter(meterPanel);
|
||||
meterLayout->addWidget(m_meter, 1);
|
||||
|
||||
auto *nodesLabel = new QLabel(QString("NODE METERS"), meterPanel);
|
||||
nodesLabel->setStyleSheet("color: #8c94a5; font-weight: 700; font-size: 10px; letter-spacing: 1px; border: none;");
|
||||
nodesLabel->setAlignment(Qt::AlignLeft);
|
||||
meterLayout->addWidget(nodesLabel);
|
||||
|
||||
m_meterScroll = new QScrollArea(meterPanel);
|
||||
m_meterScroll->setFrameShape(QFrame::NoFrame);
|
||||
m_meterScroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
m_meterScroll->setWidgetResizable(true);
|
||||
|
||||
m_meterList = new QWidget(m_meterScroll);
|
||||
m_meterListLayout = new QVBoxLayout(m_meterList);
|
||||
m_meterListLayout->setContentsMargins(0, 0, 0, 0);
|
||||
m_meterListLayout->setSpacing(10);
|
||||
m_meterList->setLayout(m_meterListLayout);
|
||||
m_meterScroll->setWidget(m_meterList);
|
||||
meterLayout->addWidget(m_meterScroll, 2);
|
||||
|
||||
meterLayout->addStretch();
|
||||
|
||||
meterPanel->setFixedWidth(160);
|
||||
meterPanel->setFixedWidth(320);
|
||||
splitter->addWidget(meterPanel);
|
||||
splitter->setStretchFactor(0, 1);
|
||||
splitter->setStretchFactor(1, 0);
|
||||
|
|
@ -175,6 +197,9 @@ GraphEditorWidget::GraphEditorWidget(Potato::PipeWireController *controller, QWi
|
|||
|
||||
static bool isAudioEndpoint(const Potato::NodeInfo &node)
|
||||
{
|
||||
if (node.name.startsWith("Potato-Meter") || node.name.startsWith("Potato-Node-Meter")) {
|
||||
return false;
|
||||
}
|
||||
return node.mediaClass == Potato::MediaClass::AudioSink
|
||||
|| node.mediaClass == Potato::MediaClass::AudioSource
|
||||
|| node.mediaClass == Potato::MediaClass::AudioDuplex
|
||||
|
|
@ -187,6 +212,7 @@ void GraphEditorWidget::syncGraph()
|
|||
for (const auto &node : nodes) {
|
||||
if (isAudioEndpoint(node)) {
|
||||
m_model->addPipeWireNode(node);
|
||||
refreshNodeMeter(node.id, node);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -215,6 +241,7 @@ void GraphEditorWidget::onNodeAdded(const Potato::NodeInfo &node)
|
|||
{
|
||||
if (isAudioEndpoint(node)) {
|
||||
m_model->addPipeWireNode(node);
|
||||
refreshNodeMeter(node.id, node);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -227,11 +254,21 @@ void GraphEditorWidget::onNodeChanged(const Potato::NodeInfo &node)
|
|||
if (!m_model->updatePipeWireNode(node)) {
|
||||
m_model->addPipeWireNode(node);
|
||||
}
|
||||
|
||||
refreshNodeMeter(node.id, node);
|
||||
}
|
||||
|
||||
void GraphEditorWidget::onNodeRemoved(uint32_t nodeId)
|
||||
{
|
||||
m_model->removePipeWireNode(nodeId);
|
||||
|
||||
m_controller->removeNodeMeter(nodeId);
|
||||
|
||||
if (m_nodeMeterRows.contains(nodeId)) {
|
||||
QWidget *row = m_nodeMeterRows.take(nodeId);
|
||||
m_nodeMeters.remove(nodeId);
|
||||
row->deleteLater();
|
||||
}
|
||||
}
|
||||
|
||||
void GraphEditorWidget::onLinkAdded(const Potato::LinkInfo &link)
|
||||
|
|
@ -345,4 +382,58 @@ void GraphEditorWidget::updateMeter()
|
|||
|
||||
const float peak = m_controller->meterPeak();
|
||||
m_meter->setLevel(peak);
|
||||
|
||||
for (auto it = m_nodeMeters.begin(); it != m_nodeMeters.end(); ++it) {
|
||||
const uint32_t nodeId = static_cast<uint32_t>(it.key());
|
||||
const float nodePeak = m_controller->nodeMeterPeak(nodeId);
|
||||
it.value()->setLevel(nodePeak);
|
||||
}
|
||||
}
|
||||
|
||||
void GraphEditorWidget::refreshNodeMeter(uint32_t nodeId, const Potato::NodeInfo &node)
|
||||
{
|
||||
if (m_nodeMeterRows.contains(nodeId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!node.name.isEmpty()) {
|
||||
bool captureSink = node.mediaClass == Potato::MediaClass::AudioSink
|
||||
|| node.mediaClass == Potato::MediaClass::AudioDuplex;
|
||||
if (!captureSink) {
|
||||
for (const auto &port : node.outputPorts) {
|
||||
if (port.name.contains("monitor", Qt::CaseInsensitive)) {
|
||||
captureSink = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
m_controller->ensureNodeMeter(nodeId, node.name, captureSink);
|
||||
}
|
||||
|
||||
auto *row = new QWidget(m_meterList);
|
||||
auto *rowLayout = new QHBoxLayout(row);
|
||||
rowLayout->setContentsMargins(0, 0, 0, 0);
|
||||
rowLayout->setSpacing(8);
|
||||
|
||||
const QString title = node.description.isEmpty() ? node.name : node.description;
|
||||
auto *label = new QLabel(title, row);
|
||||
label->setStyleSheet("color: #c7cfdd; font-size: 11px; border: none;");
|
||||
label->setToolTip(title);
|
||||
const int labelWidth = 250;
|
||||
label->setFixedWidth(labelWidth);
|
||||
QFontMetrics metrics(label->font());
|
||||
label->setText(metrics.elidedText(title, Qt::ElideRight, labelWidth));
|
||||
|
||||
auto *meter = new AudioLevelMeter(row);
|
||||
meter->setMinimumHeight(70);
|
||||
meter->setFixedWidth(26);
|
||||
|
||||
rowLayout->addWidget(label, 1);
|
||||
rowLayout->addWidget(meter, 0, Qt::AlignRight);
|
||||
row->setLayout(rowLayout);
|
||||
|
||||
m_meterListLayout->addWidget(row);
|
||||
|
||||
m_nodeMeters.insert(nodeId, meter);
|
||||
m_nodeMeterRows.insert(nodeId, row);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@
|
|||
|
||||
class AudioLevelMeter;
|
||||
class QTimer;
|
||||
class QScrollArea;
|
||||
class QVBoxLayout;
|
||||
|
||||
class GraphEditorWidget : public QWidget
|
||||
{
|
||||
|
|
@ -30,6 +32,7 @@ private slots:
|
|||
void onConnectionCreated(QtNodes::ConnectionId const connectionId);
|
||||
void onConnectionDeleted(QtNodes::ConnectionId const connectionId);
|
||||
void updateMeter();
|
||||
void refreshNodeMeter(uint32_t nodeId, const Potato::NodeInfo &node);
|
||||
|
||||
private:
|
||||
void syncGraph();
|
||||
|
|
@ -47,4 +50,9 @@ private:
|
|||
QMap<uint32_t, QString> m_linkIdToConnection;
|
||||
AudioLevelMeter *m_meter = nullptr;
|
||||
QTimer *m_meterTimer = nullptr;
|
||||
QScrollArea *m_meterScroll = nullptr;
|
||||
QWidget *m_meterList = nullptr;
|
||||
QVBoxLayout *m_meterListLayout = nullptr;
|
||||
QMap<uint32_t, AudioLevelMeter*> m_nodeMeters;
|
||||
QMap<uint32_t, QWidget*> m_nodeMeterRows;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
#include <QDebug>
|
||||
#include <QMutexLocker>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QElapsedTimer>
|
||||
#include <QThread>
|
||||
#include <cstring>
|
||||
|
|
@ -159,6 +160,58 @@ static const struct pw_stream_events meter_events = []() {
|
|||
return events;
|
||||
}();
|
||||
|
||||
struct NodeMeter {
|
||||
uint32_t nodeId;
|
||||
QString targetName;
|
||||
pw_stream *stream = nullptr;
|
||||
std::atomic<float> peak{0.0f};
|
||||
};
|
||||
|
||||
static void nodeMeterProcess(void *data)
|
||||
{
|
||||
auto *meter = static_cast<NodeMeter*>(data);
|
||||
if (!meter || !meter->stream) {
|
||||
return;
|
||||
}
|
||||
|
||||
struct pw_buffer *buf = pw_stream_dequeue_buffer(meter->stream);
|
||||
if (!buf || !buf->buffer || buf->buffer->n_datas == 0) {
|
||||
if (buf) {
|
||||
pw_stream_queue_buffer(meter->stream, buf);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
struct spa_buffer *spaBuf = buf->buffer;
|
||||
struct spa_data *data0 = &spaBuf->datas[0];
|
||||
if (!data0->data || !data0->chunk) {
|
||||
pw_stream_queue_buffer(meter->stream, 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;
|
||||
}
|
||||
}
|
||||
|
||||
meter->peak.store(peak, std::memory_order_relaxed);
|
||||
pw_stream_queue_buffer(meter->stream, buf);
|
||||
}
|
||||
|
||||
static const struct pw_stream_events node_meter_events = []() {
|
||||
struct pw_stream_events events{};
|
||||
events.version = PW_VERSION_STREAM_EVENTS;
|
||||
events.process = nodeMeterProcess;
|
||||
return events;
|
||||
}();
|
||||
|
||||
PipeWireController::PipeWireController(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
|
|
@ -256,6 +309,18 @@ void PipeWireController::shutdown()
|
|||
}
|
||||
|
||||
teardownMeterStream();
|
||||
|
||||
{
|
||||
QMutexLocker lock(&m_meterMutex);
|
||||
for (auto it = m_nodeMeters.begin(); it != m_nodeMeters.end(); ++it) {
|
||||
NodeMeter *meter = it.value();
|
||||
if (meter && meter->stream) {
|
||||
pw_stream_destroy(meter->stream);
|
||||
}
|
||||
delete meter;
|
||||
}
|
||||
m_nodeMeters.clear();
|
||||
}
|
||||
|
||||
if (m_core) {
|
||||
pw_core_disconnect(m_core);
|
||||
|
|
@ -310,6 +375,115 @@ float PipeWireController::meterPeak() const
|
|||
return m_meterPeak.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
float PipeWireController::nodeMeterPeak(uint32_t nodeId) const
|
||||
{
|
||||
QMutexLocker lock(&m_meterMutex);
|
||||
if (!m_nodeMeters.contains(nodeId)) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
NodeMeter *meter = m_nodeMeters.value(nodeId);
|
||||
if (!meter) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
return meter->peak.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void PipeWireController::ensureNodeMeter(uint32_t nodeId, const QString &targetName, bool captureSink)
|
||||
{
|
||||
if (!m_threadLoop || !m_core) {
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
QMutexLocker lock(&m_meterMutex);
|
||||
if (m_nodeMeters.contains(nodeId)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
auto *meter = new NodeMeter;
|
||||
meter->nodeId = nodeId;
|
||||
meter->targetName = targetName;
|
||||
|
||||
const QByteArray targetNameBytes = meter->targetName.toUtf8();
|
||||
struct pw_properties *props = pw_properties_new(
|
||||
PW_KEY_MEDIA_TYPE, "Audio",
|
||||
PW_KEY_MEDIA_CATEGORY, "Capture",
|
||||
PW_KEY_MEDIA_CLASS, "Stream/Input/Audio",
|
||||
PW_KEY_TARGET_OBJECT, targetNameBytes.constData(),
|
||||
PW_KEY_STREAM_MONITOR, "true",
|
||||
nullptr);
|
||||
|
||||
if (captureSink) {
|
||||
pw_properties_set(props, PW_KEY_STREAM_CAPTURE_SINK, "true");
|
||||
}
|
||||
|
||||
lock();
|
||||
|
||||
meter->stream = pw_stream_new_simple(
|
||||
pw_thread_loop_get_loop(m_threadLoop),
|
||||
"Potato-Node-Meter",
|
||||
props,
|
||||
&node_meter_events,
|
||||
meter);
|
||||
|
||||
if (!meter->stream) {
|
||||
pw_properties_free(props);
|
||||
unlock();
|
||||
delete meter;
|
||||
return;
|
||||
}
|
||||
|
||||
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(
|
||||
meter->stream,
|
||||
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);
|
||||
|
||||
if (res != 0) {
|
||||
pw_stream_destroy(meter->stream);
|
||||
unlock();
|
||||
delete meter;
|
||||
return;
|
||||
}
|
||||
|
||||
unlock();
|
||||
|
||||
QMutexLocker lock(&m_meterMutex);
|
||||
m_nodeMeters.insert(nodeId, meter);
|
||||
}
|
||||
|
||||
void PipeWireController::removeNodeMeter(uint32_t nodeId)
|
||||
{
|
||||
QMutexLocker lock(&m_meterMutex);
|
||||
if (!m_nodeMeters.contains(nodeId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
NodeMeter *meter = m_nodeMeters.take(nodeId);
|
||||
if (meter && meter->stream) {
|
||||
pw_stream_destroy(meter->stream);
|
||||
}
|
||||
delete meter;
|
||||
}
|
||||
|
||||
uint32_t PipeWireController::createLink(uint32_t outputNodeId, uint32_t outputPortId,
|
||||
uint32_t inputNodeId, uint32_t inputPortId)
|
||||
{
|
||||
|
|
@ -611,7 +785,9 @@ bool PipeWireController::setupMeterStream()
|
|||
struct pw_properties *props = pw_properties_new(
|
||||
PW_KEY_MEDIA_TYPE, "Audio",
|
||||
PW_KEY_MEDIA_CATEGORY, "Capture",
|
||||
PW_KEY_MEDIA_CLASS, "Audio/Source",
|
||||
PW_KEY_MEDIA_CLASS, "Stream/Input/Audio",
|
||||
PW_KEY_STREAM_CAPTURE_SINK, "true",
|
||||
PW_KEY_STREAM_MONITOR, "true",
|
||||
nullptr);
|
||||
|
||||
m_meterStream = pw_stream_new_simple(
|
||||
|
|
|
|||
|
|
@ -14,9 +14,10 @@ struct pw_registry;
|
|||
struct pw_stream;
|
||||
struct spa_hook;
|
||||
struct spa_dict;
|
||||
|
||||
namespace Potato {
|
||||
|
||||
struct NodeMeter;
|
||||
|
||||
class PipeWireController : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
|
@ -34,6 +35,9 @@ public:
|
|||
NodeInfo nodeById(uint32_t id) const;
|
||||
QVector<LinkInfo> links() const;
|
||||
float meterPeak() const;
|
||||
float nodeMeterPeak(uint32_t nodeId) const;
|
||||
void ensureNodeMeter(uint32_t nodeId, const QString &targetName, bool captureSink);
|
||||
void removeNodeMeter(uint32_t nodeId);
|
||||
|
||||
uint32_t createLink(uint32_t outputNodeId, uint32_t outputPortId,
|
||||
uint32_t inputNodeId, uint32_t inputPortId);
|
||||
|
|
@ -89,6 +93,9 @@ private:
|
|||
QAtomicInteger<bool> m_connected{false};
|
||||
QAtomicInteger<bool> m_initialized{false};
|
||||
std::atomic<float> m_meterPeak{0.0f};
|
||||
|
||||
mutable QMutex m_meterMutex;
|
||||
QMap<uint32_t, NodeMeter*> m_nodeMeters;
|
||||
|
||||
uint32_t m_nodeIdCounter = 0;
|
||||
uint32_t m_linkIdCounter = 0;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue