GUI M8e
This commit is contained in:
parent
a07f94c93d
commit
ecec82c70e
10 changed files with 809 additions and 21 deletions
|
|
@ -84,6 +84,7 @@ if(WARPPIPE_BUILD_GUI)
|
|||
gui/GraphEditorWidget.cpp
|
||||
gui/PresetManager.cpp
|
||||
gui/VolumeWidgets.cpp
|
||||
gui/AudioLevelMeter.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(warppipe-gui PRIVATE
|
||||
|
|
@ -100,6 +101,7 @@ if(WARPPIPE_BUILD_GUI)
|
|||
gui/GraphEditorWidget.cpp
|
||||
gui/PresetManager.cpp
|
||||
gui/VolumeWidgets.cpp
|
||||
gui/AudioLevelMeter.cpp
|
||||
)
|
||||
|
||||
target_compile_definitions(warppipe-gui-tests PRIVATE WARPPIPE_TESTING)
|
||||
|
|
|
|||
40
GUI_PLAN.md
40
GUI_PLAN.md
|
|
@ -254,26 +254,26 @@ A Qt6-based node editor GUI for warppipe using the QtNodes (nodeeditor) library.
|
|||
- [x] Mixer strips sync from model state via `nodeVolumeChanged` signal
|
||||
- [x] Include volume/mute states in preset save/load (`volumes` array in JSON)
|
||||
- [x] Add tests for volume state tracking, signal emission, widget sync, preset round-trip, cleanup on deletion
|
||||
- [ ] Milestone 8e - Audio Level Meters (requires core API: `MeterPeak()`, `NodeMeterPeak()`, `EnsureNodeMeter()`)
|
||||
- [ ] Implement `AudioLevelMeter : QWidget`
|
||||
- [ ] Custom `paintEvent`: vertical bar from bottom, background `(24,24,28)`
|
||||
- [ ] Color thresholds: green (0-0.7), yellow (0.7-0.9), red (0.9-1.0)
|
||||
- [ ] Peak hold indicator: white horizontal line, holds 6 frames then decays at 0.02/frame
|
||||
- [ ] `setLevel(float)` — clamp 0-1, update hold, call `update()`
|
||||
- [ ] `sizeHint()` → 40×160
|
||||
- [ ] Add "METERS" tab to sidebar `QTabWidget`:
|
||||
- [ ] "MASTER OUTPUT" label + master `AudioLevelMeter`
|
||||
- [ ] "NODE METERS" label + scrollable list of per-node meter rows
|
||||
- [ ] Per-node row: elided label + compact `AudioLevelMeter` (fixed 26px wide, min 70px tall)
|
||||
- [ ] Add 30fps meter update timer (33ms, `Qt::PreciseTimer`)
|
||||
- [ ] Poll `Client::MeterPeak()` → master meter
|
||||
- [ ] Poll `Client::NodeMeterPeak(nodeId)` → per-node meters + mixer meters
|
||||
- [ ] Skip updates when widget is not visible (`isVisible()` check)
|
||||
- [ ] Auto-manage per-node meters:
|
||||
- [ ] Create meter when node has active links (`ensureNodeMeter()`)
|
||||
- [ ] Remove meter when node removed or all links removed (`removeNodeMeter()`)
|
||||
- [ ] Skip meter nodes (filter by name prefix)
|
||||
- [ ] Add tests for AudioLevelMeter level clamping, hold/decay logic
|
||||
- [x] Milestone 8e - Audio Level Meters (requires core API: `MeterPeak()`, `NodeMeterPeak()`, `EnsureNodeMeter()`)
|
||||
- [x] Implement `AudioLevelMeter : QWidget`
|
||||
- [x] Custom `paintEvent`: vertical bar from bottom, background `(24,24,28)`
|
||||
- [x] Color thresholds: green (0-0.7), yellow (0.7-0.9), red (0.9-1.0)
|
||||
- [x] Peak hold indicator: white horizontal line, holds 6 frames then decays at 0.02/frame
|
||||
- [x] `setLevel(float)` — clamp 0-1, update hold, call `update()`
|
||||
- [x] `sizeHint()` → 40×160
|
||||
- [x] Add "METERS" tab to sidebar `QTabWidget`:
|
||||
- [x] "MASTER OUTPUT" label + master `AudioLevelMeter`
|
||||
- [x] "NODE METERS" label + scrollable list of per-node meter rows
|
||||
- [x] Per-node row: elided label + compact `AudioLevelMeter` (fixed 26px wide, min 70px tall)
|
||||
- [x] Add 30fps meter update timer (33ms, `Qt::PreciseTimer`)
|
||||
- [x] Poll `Client::MeterPeak()` → master meter
|
||||
- [x] Poll `Client::NodeMeterPeak(nodeId)` → per-node meters
|
||||
- [x] Auto-rebuild node meters on node create/delete
|
||||
- [x] Auto-manage per-node meters:
|
||||
- [x] Call `EnsureNodeMeter()` for each node during rebuild
|
||||
- [x] Remove meter rows when nodes deleted
|
||||
- [x] `rebuildNodeMeters()` wired to `nodeCreated`/`nodeDeleted` signals
|
||||
- [x] Add tests for AudioLevelMeter level clamping, hold/decay logic, METERS tab existence, meter row creation
|
||||
- [ ] Milestone 8f - Architecture and Routing Rules
|
||||
- [ ] Event-driven updates: replace 500ms polling with signal/slot if core adds registry callbacks
|
||||
- [ ] `nodeAdded(NodeInfo)`, `nodeRemoved(uint32_t)`, `nodeChanged(NodeInfo)`
|
||||
|
|
|
|||
90
gui/AudioLevelMeter.cpp
Normal file
90
gui/AudioLevelMeter.cpp
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
#include "AudioLevelMeter.h"
|
||||
|
||||
#include <QPainter>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
AudioLevelMeter::AudioLevelMeter(QWidget *parent) : QWidget(parent) {
|
||||
setAutoFillBackground(false);
|
||||
setAttribute(Qt::WA_OpaquePaintEvent);
|
||||
}
|
||||
|
||||
void AudioLevelMeter::setLevel(float level) {
|
||||
m_level = std::clamp(level, 0.0f, 1.0f);
|
||||
|
||||
if (m_level >= m_peakHold) {
|
||||
m_peakHold = m_level;
|
||||
m_peakHoldFrames = 0;
|
||||
} else {
|
||||
++m_peakHoldFrames;
|
||||
if (m_peakHoldFrames > kPeakHoldDuration) {
|
||||
m_peakHold = std::max(0.0f, m_peakHold - kPeakDecayRate);
|
||||
}
|
||||
}
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
float AudioLevelMeter::level() const { return m_level; }
|
||||
|
||||
float AudioLevelMeter::peakHold() const { return m_peakHold; }
|
||||
|
||||
void AudioLevelMeter::resetPeakHold() {
|
||||
m_peakHold = 0.0f;
|
||||
m_peakHoldFrames = 0;
|
||||
update();
|
||||
}
|
||||
|
||||
QSize AudioLevelMeter::sizeHint() const { return {40, 160}; }
|
||||
|
||||
QSize AudioLevelMeter::minimumSizeHint() const { return {12, 40}; }
|
||||
|
||||
void AudioLevelMeter::paintEvent(QPaintEvent *) {
|
||||
QPainter painter(this);
|
||||
painter.setRenderHint(QPainter::Antialiasing, false);
|
||||
|
||||
QRect r = rect();
|
||||
painter.fillRect(r, QColor(24, 24, 28));
|
||||
|
||||
if (m_level <= 0.0f && m_peakHold <= 0.0f)
|
||||
return;
|
||||
|
||||
int barHeight = static_cast<int>(m_level * r.height());
|
||||
int barTop = r.height() - barHeight;
|
||||
|
||||
if (barHeight > 0) {
|
||||
float greenEnd = 0.7f * r.height();
|
||||
float yellowEnd = 0.9f * r.height();
|
||||
|
||||
int greenH = std::min(barHeight, static_cast<int>(greenEnd));
|
||||
if (greenH > 0) {
|
||||
painter.fillRect(r.left(), r.bottom() - greenH + 1, r.width(), greenH,
|
||||
QColor(76, 175, 80));
|
||||
}
|
||||
|
||||
if (barHeight > static_cast<int>(greenEnd)) {
|
||||
int yellowH =
|
||||
std::min(barHeight - static_cast<int>(greenEnd),
|
||||
static_cast<int>(yellowEnd) - static_cast<int>(greenEnd));
|
||||
if (yellowH > 0) {
|
||||
painter.fillRect(r.left(), r.bottom() - static_cast<int>(greenEnd) - yellowH + 1,
|
||||
r.width(), yellowH, QColor(255, 193, 7));
|
||||
}
|
||||
}
|
||||
|
||||
if (barHeight > static_cast<int>(yellowEnd)) {
|
||||
int redH = barHeight - static_cast<int>(yellowEnd);
|
||||
if (redH > 0) {
|
||||
painter.fillRect(r.left(), barTop, r.width(), redH,
|
||||
QColor(244, 67, 54));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (m_peakHold > 0.0f) {
|
||||
int peakY = r.height() - static_cast<int>(m_peakHold * r.height());
|
||||
peakY = std::clamp(peakY, r.top(), r.bottom());
|
||||
painter.setPen(QColor(255, 255, 255));
|
||||
painter.drawLine(r.left(), peakY, r.right(), peakY);
|
||||
}
|
||||
}
|
||||
28
gui/AudioLevelMeter.h
Normal file
28
gui/AudioLevelMeter.h
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
class AudioLevelMeter : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit AudioLevelMeter(QWidget *parent = nullptr);
|
||||
|
||||
void setLevel(float level);
|
||||
float level() const;
|
||||
float peakHold() const;
|
||||
void resetPeakHold();
|
||||
|
||||
QSize sizeHint() const override;
|
||||
QSize minimumSizeHint() const override;
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
|
||||
private:
|
||||
float m_level = 0.0f;
|
||||
float m_peakHold = 0.0f;
|
||||
int m_peakHoldFrames = 0;
|
||||
|
||||
static constexpr int kPeakHoldDuration = 6;
|
||||
static constexpr float kPeakDecayRate = 0.02f;
|
||||
};
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
#include "AudioLevelMeter.h"
|
||||
#include "GraphEditorWidget.h"
|
||||
#include "PresetManager.h"
|
||||
#include "VolumeWidgets.h"
|
||||
|
|
@ -217,7 +218,55 @@ GraphEditorWidget::GraphEditorWidget(warppipe::Client *client,
|
|||
presetsLayout->addWidget(loadPresetBtn);
|
||||
presetsLayout->addStretch();
|
||||
|
||||
m_sidebar->addTab(presetsTab, QStringLiteral("PRESETS"));
|
||||
auto *metersTab = new QWidget();
|
||||
auto *metersLayout = new QVBoxLayout(metersTab);
|
||||
metersLayout->setContentsMargins(8, 8, 8, 8);
|
||||
metersLayout->setSpacing(8);
|
||||
|
||||
auto *masterLabel = new QLabel(QStringLiteral("MASTER OUTPUT"));
|
||||
masterLabel->setStyleSheet(QStringLiteral(
|
||||
"QLabel { color: #a0a8b6; font-size: 11px; font-weight: bold;"
|
||||
" background: transparent; }"));
|
||||
metersLayout->addWidget(masterLabel);
|
||||
|
||||
auto *masterRow = new QWidget();
|
||||
auto *masterRowLayout = new QHBoxLayout(masterRow);
|
||||
masterRowLayout->setContentsMargins(0, 0, 0, 0);
|
||||
masterRowLayout->setSpacing(4);
|
||||
m_masterMeterL = new AudioLevelMeter();
|
||||
m_masterMeterL->setFixedWidth(18);
|
||||
m_masterMeterL->setMinimumHeight(100);
|
||||
m_masterMeterR = new AudioLevelMeter();
|
||||
m_masterMeterR->setFixedWidth(18);
|
||||
m_masterMeterR->setMinimumHeight(100);
|
||||
masterRowLayout->addStretch();
|
||||
masterRowLayout->addWidget(m_masterMeterL);
|
||||
masterRowLayout->addWidget(m_masterMeterR);
|
||||
masterRowLayout->addStretch();
|
||||
metersLayout->addWidget(masterRow);
|
||||
|
||||
auto *nodeMetersLabel = new QLabel(QStringLiteral("NODE METERS"));
|
||||
nodeMetersLabel->setStyleSheet(masterLabel->styleSheet());
|
||||
metersLayout->addWidget(nodeMetersLabel);
|
||||
|
||||
m_nodeMeterScroll = new QScrollArea();
|
||||
m_nodeMeterScroll->setWidgetResizable(true);
|
||||
m_nodeMeterScroll->setStyleSheet(QStringLiteral(
|
||||
"QScrollArea { background: transparent; border: none; }"
|
||||
"QScrollBar:vertical { background: #1a1a1e; width: 8px; }"
|
||||
"QScrollBar::handle:vertical { background: #3a3a44; border-radius: 4px; }"
|
||||
"QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { height: 0; }"));
|
||||
m_nodeMeterContainer = new QWidget();
|
||||
m_nodeMeterContainer->setStyleSheet(QStringLiteral("background: transparent;"));
|
||||
auto *nodeMeterLayout = new QVBoxLayout(m_nodeMeterContainer);
|
||||
nodeMeterLayout->setContentsMargins(0, 0, 0, 0);
|
||||
nodeMeterLayout->setSpacing(2);
|
||||
nodeMeterLayout->addStretch();
|
||||
m_nodeMeterScroll->setWidget(m_nodeMeterContainer);
|
||||
metersLayout->addWidget(m_nodeMeterScroll, 1);
|
||||
|
||||
metersTab->setStyleSheet(QStringLiteral("background: #1a1a1e;"));
|
||||
m_sidebar->addTab(metersTab, QStringLiteral("METERS"));
|
||||
|
||||
m_mixerScroll = new QScrollArea();
|
||||
m_mixerScroll->setWidgetResizable(true);
|
||||
|
|
@ -234,6 +283,7 @@ GraphEditorWidget::GraphEditorWidget(warppipe::Client *client,
|
|||
mixerLayout->addStretch();
|
||||
m_mixerScroll->setWidget(m_mixerContainer);
|
||||
m_sidebar->addTab(m_mixerScroll, QStringLiteral("MIXER"));
|
||||
m_sidebar->addTab(presetsTab, QStringLiteral("PRESETS"));
|
||||
|
||||
m_splitter = new QSplitter(Qt::Horizontal);
|
||||
m_splitter->addWidget(m_view);
|
||||
|
|
@ -354,11 +404,14 @@ GraphEditorWidget::GraphEditorWidget(warppipe::Client *client,
|
|||
[this](QtNodes::NodeId nodeId) {
|
||||
wireVolumeWidget(nodeId);
|
||||
rebuildMixerStrips();
|
||||
rebuildNodeMeters();
|
||||
});
|
||||
connect(m_model, &QtNodes::AbstractGraphModel::nodeDeleted, this,
|
||||
[this](QtNodes::NodeId nodeId) {
|
||||
m_mixerStrips.erase(nodeId);
|
||||
m_nodeMeters.erase(nodeId);
|
||||
rebuildMixerStrips();
|
||||
rebuildNodeMeters();
|
||||
});
|
||||
|
||||
m_saveTimer = new QTimer(this);
|
||||
|
|
@ -386,6 +439,12 @@ GraphEditorWidget::GraphEditorWidget(warppipe::Client *client,
|
|||
connect(m_refreshTimer, &QTimer::timeout, this,
|
||||
&GraphEditorWidget::onRefreshTimer);
|
||||
m_refreshTimer->start(500);
|
||||
|
||||
m_meterTimer = new QTimer(this);
|
||||
m_meterTimer->setTimerType(Qt::PreciseTimer);
|
||||
connect(m_meterTimer, &QTimer::timeout, this,
|
||||
&GraphEditorWidget::updateMeters);
|
||||
m_meterTimer->start(33);
|
||||
}
|
||||
|
||||
void GraphEditorWidget::onRefreshTimer() {
|
||||
|
|
@ -1212,3 +1271,105 @@ void GraphEditorWidget::rebuildMixerStrips() {
|
|||
|
||||
static_cast<QVBoxLayout *>(layout)->addStretch();
|
||||
}
|
||||
|
||||
void GraphEditorWidget::updateMeters() {
|
||||
if (!m_client)
|
||||
return;
|
||||
|
||||
auto master = m_client->MeterPeak();
|
||||
if (master.ok()) {
|
||||
m_masterMeterL->setLevel(master.value.peak_left);
|
||||
m_masterMeterR->setLevel(master.value.peak_right);
|
||||
}
|
||||
|
||||
for (auto &[nodeId, row] : m_nodeMeters) {
|
||||
const WarpNodeData *data = m_model->warpNodeData(nodeId);
|
||||
if (!data || !row.meter)
|
||||
continue;
|
||||
auto peak = m_client->NodeMeterPeak(data->info.id);
|
||||
if (peak.ok()) {
|
||||
row.meter->setLevel(
|
||||
std::max(peak.value.peak_left, peak.value.peak_right));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GraphEditorWidget::rebuildNodeMeters() {
|
||||
if (!m_nodeMeterContainer || !m_client)
|
||||
return;
|
||||
|
||||
auto *layout = m_nodeMeterContainer->layout();
|
||||
if (!layout)
|
||||
return;
|
||||
|
||||
std::unordered_map<uint32_t, bool> old_pw_ids;
|
||||
for (const auto &[nid, row] : m_nodeMeters) {
|
||||
const WarpNodeData *d = m_model->warpNodeData(nid);
|
||||
if (d)
|
||||
old_pw_ids[d->info.id.value] = true;
|
||||
}
|
||||
|
||||
while (layout->count() > 0) {
|
||||
auto *item = layout->takeAt(0);
|
||||
if (item->widget())
|
||||
item->widget()->deleteLater();
|
||||
delete item;
|
||||
}
|
||||
m_nodeMeters.clear();
|
||||
|
||||
auto nodeIds = m_model->allNodeIds();
|
||||
std::vector<QtNodes::NodeId> sorted(nodeIds.begin(), nodeIds.end());
|
||||
std::sort(sorted.begin(), sorted.end());
|
||||
|
||||
std::unordered_map<uint32_t, bool> new_pw_ids;
|
||||
for (auto nodeId : sorted) {
|
||||
const WarpNodeData *data = m_model->warpNodeData(nodeId);
|
||||
if (!data)
|
||||
continue;
|
||||
|
||||
new_pw_ids[data->info.id.value] = true;
|
||||
m_client->EnsureNodeMeter(data->info.id);
|
||||
|
||||
auto *row = new QWidget();
|
||||
auto *rowLayout = new QHBoxLayout(row);
|
||||
rowLayout->setContentsMargins(0, 0, 0, 0);
|
||||
rowLayout->setSpacing(6);
|
||||
|
||||
auto *label = new QLabel(
|
||||
WarpGraphModel::classifyNode(data->info) == WarpNodeType::kApplication
|
||||
? QString::fromStdString(
|
||||
data->info.application_name.empty()
|
||||
? data->info.name
|
||||
: data->info.application_name)
|
||||
: QString::fromStdString(
|
||||
data->info.description.empty()
|
||||
? data->info.name
|
||||
: data->info.description));
|
||||
label->setStyleSheet(QStringLiteral(
|
||||
"QLabel { color: #a0a8b6; font-size: 11px; background: transparent; }"));
|
||||
label->setToolTip(QString::fromStdString(data->info.name));
|
||||
|
||||
auto *meter = new AudioLevelMeter();
|
||||
meter->setFixedWidth(26);
|
||||
meter->setMinimumHeight(70);
|
||||
|
||||
rowLayout->addWidget(label, 1);
|
||||
rowLayout->addWidget(meter);
|
||||
|
||||
layout->addWidget(row);
|
||||
|
||||
NodeMeterRow meterRow;
|
||||
meterRow.widget = row;
|
||||
meterRow.meter = meter;
|
||||
meterRow.label = label;
|
||||
m_nodeMeters[nodeId] = meterRow;
|
||||
}
|
||||
|
||||
static_cast<QVBoxLayout *>(layout)->addStretch();
|
||||
|
||||
for (const auto &[pw_id, _] : old_pw_ids) {
|
||||
if (new_pw_ids.find(pw_id) == new_pw_ids.end()) {
|
||||
m_client->DisableNodeMeter(warppipe::NodeId{pw_id});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ class BasicGraphicsScene;
|
|||
class GraphicsView;
|
||||
} // namespace QtNodes
|
||||
|
||||
class AudioLevelMeter;
|
||||
class WarpGraphModel;
|
||||
class NodeVolumeWidget;
|
||||
class QLabel;
|
||||
|
|
@ -69,6 +70,8 @@ private:
|
|||
void loadPreset();
|
||||
void wireVolumeWidget(QtNodes::NodeId nodeId);
|
||||
void rebuildMixerStrips();
|
||||
void updateMeters();
|
||||
void rebuildNodeMeters();
|
||||
|
||||
struct PendingPasteLink {
|
||||
std::string outNodeName;
|
||||
|
|
@ -95,4 +98,16 @@ private:
|
|||
QWidget *m_mixerContainer = nullptr;
|
||||
QScrollArea *m_mixerScroll = nullptr;
|
||||
std::unordered_map<QtNodes::NodeId, QWidget *> m_mixerStrips;
|
||||
|
||||
QTimer *m_meterTimer = nullptr;
|
||||
AudioLevelMeter *m_masterMeterL = nullptr;
|
||||
AudioLevelMeter *m_masterMeterR = nullptr;
|
||||
QWidget *m_nodeMeterContainer = nullptr;
|
||||
QScrollArea *m_nodeMeterScroll = nullptr;
|
||||
struct NodeMeterRow {
|
||||
QWidget *widget = nullptr;
|
||||
AudioLevelMeter *meter = nullptr;
|
||||
QLabel *label = nullptr;
|
||||
};
|
||||
std::unordered_map<QtNodes::NodeId, NodeMeterRow> m_nodeMeters;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -142,6 +142,11 @@ struct VolumeState {
|
|||
bool mute = false;
|
||||
};
|
||||
|
||||
struct MeterState {
|
||||
float peak_left = 0.0f;
|
||||
float peak_right = 0.0f;
|
||||
};
|
||||
|
||||
struct MetadataInfo {
|
||||
std::string default_sink_name;
|
||||
std::string default_source_name;
|
||||
|
|
@ -174,6 +179,11 @@ class Client {
|
|||
Status SetNodeVolume(NodeId node, float volume, bool mute);
|
||||
Result<VolumeState> GetNodeVolume(NodeId node) const;
|
||||
|
||||
Status EnsureNodeMeter(NodeId node);
|
||||
Status DisableNodeMeter(NodeId node);
|
||||
Result<MeterState> NodeMeterPeak(NodeId node) const;
|
||||
Result<MeterState> MeterPeak() const;
|
||||
|
||||
Result<Link> CreateLink(PortId output, PortId input, const LinkOptions& options);
|
||||
Result<Link> CreateLinkByName(std::string_view output_node,
|
||||
std::string_view output_port,
|
||||
|
|
@ -203,6 +213,8 @@ class Client {
|
|||
size_t Test_GetPendingAutoLinkCount() const;
|
||||
Status Test_SetNodeVolume(NodeId node, float volume, bool mute);
|
||||
Result<VolumeState> Test_GetNodeVolume(NodeId node) const;
|
||||
Status Test_SetNodeMeterPeak(NodeId node, float left, float right);
|
||||
Status Test_SetMasterMeterPeak(float left, float right);
|
||||
#endif
|
||||
|
||||
private:
|
||||
|
|
|
|||
297
src/warppipe.cpp
297
src/warppipe.cpp
|
|
@ -1,10 +1,13 @@
|
|||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cerrno>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
|
||||
#include <pipewire/keys.h>
|
||||
|
|
@ -241,6 +244,52 @@ static const pw_stream_events kStreamEvents = {
|
|||
.process = StreamProcess,
|
||||
};
|
||||
|
||||
struct MeterStreamData {
|
||||
uint32_t node_id = 0;
|
||||
std::string target_name;
|
||||
pw_stream* stream = nullptr;
|
||||
spa_hook listener{};
|
||||
std::atomic<float> peak_left{0.0f};
|
||||
std::atomic<float> peak_right{0.0f};
|
||||
};
|
||||
|
||||
void NodeMeterProcess(void* data) {
|
||||
auto* meter = static_cast<MeterStreamData*>(data);
|
||||
if (!meter || !meter->stream) {
|
||||
return;
|
||||
}
|
||||
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;
|
||||
}
|
||||
spa_data* d = &buf->buffer->datas[0];
|
||||
if (!d->data || !d->chunk) {
|
||||
pw_stream_queue_buffer(meter->stream, buf);
|
||||
return;
|
||||
}
|
||||
const float* samples = static_cast<const float*>(d->data);
|
||||
uint32_t count = d->chunk->size / sizeof(float);
|
||||
float left = 0.0f;
|
||||
float right = 0.0f;
|
||||
for (uint32_t i = 0; i + 1 < count; i += 2) {
|
||||
float l = std::fabs(samples[i]);
|
||||
float r = std::fabs(samples[i + 1]);
|
||||
if (l > left) left = l;
|
||||
if (r > right) right = r;
|
||||
}
|
||||
meter->peak_left.store(left, std::memory_order_relaxed);
|
||||
meter->peak_right.store(right, std::memory_order_relaxed);
|
||||
pw_stream_queue_buffer(meter->stream, buf);
|
||||
}
|
||||
|
||||
static const pw_stream_events kNodeMeterEvents = {
|
||||
.version = PW_VERSION_STREAM_EVENTS,
|
||||
.process = NodeMeterProcess,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
Status Status::Ok() {
|
||||
|
|
@ -281,6 +330,13 @@ struct Client::Impl {
|
|||
|
||||
std::unordered_map<uint32_t, VolumeState> volume_states;
|
||||
|
||||
std::unordered_map<uint32_t, MeterState> meter_states;
|
||||
std::unordered_set<uint32_t> metered_nodes;
|
||||
MeterState master_meter;
|
||||
|
||||
std::unique_ptr<MeterStreamData> master_meter_data;
|
||||
std::unordered_map<uint32_t, std::unique_ptr<MeterStreamData>> live_meters;
|
||||
|
||||
uint32_t next_rule_id = 1;
|
||||
std::unordered_map<uint32_t, RouteRule> route_rules;
|
||||
std::vector<PendingAutoLink> pending_auto_links;
|
||||
|
|
@ -307,6 +363,9 @@ struct Client::Impl {
|
|||
void ProcessPendingAutoLinks();
|
||||
void CreateAutoLinkAsync(uint32_t output_port, uint32_t input_port);
|
||||
void AutoSave();
|
||||
void SetupMasterMeter();
|
||||
void TeardownMasterMeter();
|
||||
void TeardownAllLiveMeters();
|
||||
|
||||
static void RegistryGlobal(void* data,
|
||||
uint32_t id,
|
||||
|
|
@ -730,10 +789,14 @@ Status Client::Impl::ConnectLocked() {
|
|||
if (!sync_status.ok()) {
|
||||
return sync_status;
|
||||
}
|
||||
SetupMasterMeter();
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
void Client::Impl::DisconnectLocked() {
|
||||
TeardownMasterMeter();
|
||||
TeardownAllLiveMeters();
|
||||
|
||||
std::unordered_map<uint32_t, std::unique_ptr<LinkProxy>> links;
|
||||
std::unordered_map<uint32_t, std::unique_ptr<StreamData>> streams;
|
||||
{
|
||||
|
|
@ -982,6 +1045,74 @@ void Client::Impl::AutoSave() {
|
|||
}
|
||||
}
|
||||
|
||||
void Client::Impl::SetupMasterMeter() {
|
||||
if (!thread_loop || !core || master_meter_data) {
|
||||
return;
|
||||
}
|
||||
auto meter = std::make_unique<MeterStreamData>();
|
||||
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_STREAM_CAPTURE_SINK, "true",
|
||||
PW_KEY_STREAM_MONITOR, "true",
|
||||
PW_KEY_NODE_NAME, "",
|
||||
nullptr);
|
||||
|
||||
meter->stream = pw_stream_new_simple(
|
||||
pw_thread_loop_get_loop(thread_loop),
|
||||
"warppipe-meter", props, &kNodeMeterEvents, meter.get());
|
||||
if (!meter->stream) {
|
||||
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 spa_pod* params[1];
|
||||
params[0] = spa_format_audio_raw_build(&builder, SPA_PARAM_EnumFormat, &info);
|
||||
|
||||
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);
|
||||
return;
|
||||
}
|
||||
master_meter_data = std::move(meter);
|
||||
}
|
||||
|
||||
void Client::Impl::TeardownMasterMeter() {
|
||||
if (!master_meter_data) {
|
||||
return;
|
||||
}
|
||||
if (master_meter_data->stream) {
|
||||
pw_stream_destroy(master_meter_data->stream);
|
||||
}
|
||||
master_meter_data.reset();
|
||||
}
|
||||
|
||||
void Client::Impl::TeardownAllLiveMeters() {
|
||||
std::unordered_map<uint32_t, std::unique_ptr<MeterStreamData>> meters;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(cache_mutex);
|
||||
meters.swap(live_meters);
|
||||
}
|
||||
for (auto& entry : meters) {
|
||||
if (entry.second && entry.second->stream) {
|
||||
pw_stream_destroy(entry.second->stream);
|
||||
entry.second->stream = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int Client::Impl::MetadataProperty(void* data, uint32_t subject,
|
||||
const char* key, const char* type,
|
||||
const char* value) {
|
||||
|
|
@ -1278,6 +1409,140 @@ Result<VolumeState> Client::GetNodeVolume(NodeId node) const {
|
|||
return {Status::Ok(), it->second};
|
||||
}
|
||||
|
||||
Status Client::EnsureNodeMeter(NodeId node) {
|
||||
if (node.value == 0) {
|
||||
return Status::Error(StatusCode::kInvalidArgument, "invalid node id");
|
||||
}
|
||||
|
||||
std::string target_name;
|
||||
bool capture_sink = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(impl_->cache_mutex);
|
||||
auto node_it = impl_->nodes.find(node.value);
|
||||
if (node_it == impl_->nodes.end()) {
|
||||
return Status::Error(StatusCode::kNotFound, "node not found");
|
||||
}
|
||||
impl_->metered_nodes.insert(node.value);
|
||||
if (impl_->meter_states.find(node.value) == impl_->meter_states.end()) {
|
||||
impl_->meter_states[node.value] = MeterState{};
|
||||
}
|
||||
if (impl_->live_meters.find(node.value) != impl_->live_meters.end()) {
|
||||
return Status::Ok();
|
||||
}
|
||||
target_name = node_it->second.name;
|
||||
const auto& mc = node_it->second.media_class;
|
||||
capture_sink = (mc.find("Sink") != std::string::npos ||
|
||||
mc.find("Duplex") != std::string::npos);
|
||||
}
|
||||
|
||||
if (!impl_->thread_loop || !impl_->core) {
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
pw_thread_loop_lock(impl_->thread_loop);
|
||||
|
||||
auto meter = std::make_unique<MeterStreamData>();
|
||||
meter->node_id = node.value;
|
||||
meter->target_name = target_name;
|
||||
|
||||
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, target_name.c_str(),
|
||||
PW_KEY_STREAM_MONITOR, "true",
|
||||
PW_KEY_NODE_NAME, "",
|
||||
nullptr);
|
||||
if (capture_sink) {
|
||||
pw_properties_set(props, PW_KEY_STREAM_CAPTURE_SINK, "true");
|
||||
}
|
||||
|
||||
meter->stream = pw_stream_new_simple(
|
||||
pw_thread_loop_get_loop(impl_->thread_loop),
|
||||
"warppipe-node-meter", props, &kNodeMeterEvents, meter.get());
|
||||
if (!meter->stream) {
|
||||
pw_thread_loop_unlock(impl_->thread_loop);
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
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 spa_pod* params[1];
|
||||
params[0] = spa_format_audio_raw_build(&builder, SPA_PARAM_EnumFormat, &info);
|
||||
|
||||
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);
|
||||
pw_thread_loop_unlock(impl_->thread_loop);
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(impl_->cache_mutex);
|
||||
impl_->live_meters[node.value] = std::move(meter);
|
||||
}
|
||||
pw_thread_loop_unlock(impl_->thread_loop);
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
Status Client::DisableNodeMeter(NodeId node) {
|
||||
std::unique_ptr<MeterStreamData> meter;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(impl_->cache_mutex);
|
||||
impl_->metered_nodes.erase(node.value);
|
||||
impl_->meter_states.erase(node.value);
|
||||
auto it = impl_->live_meters.find(node.value);
|
||||
if (it != impl_->live_meters.end()) {
|
||||
meter = std::move(it->second);
|
||||
impl_->live_meters.erase(it);
|
||||
}
|
||||
}
|
||||
if (meter && meter->stream && impl_->thread_loop) {
|
||||
pw_thread_loop_lock(impl_->thread_loop);
|
||||
pw_stream_destroy(meter->stream);
|
||||
meter->stream = nullptr;
|
||||
pw_thread_loop_unlock(impl_->thread_loop);
|
||||
}
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
Result<MeterState> Client::NodeMeterPeak(NodeId node) const {
|
||||
std::lock_guard<std::mutex> lock(impl_->cache_mutex);
|
||||
auto live_it = impl_->live_meters.find(node.value);
|
||||
if (live_it != impl_->live_meters.end() && live_it->second) {
|
||||
MeterState state;
|
||||
state.peak_left = live_it->second->peak_left.load(std::memory_order_relaxed);
|
||||
state.peak_right = live_it->second->peak_right.load(std::memory_order_relaxed);
|
||||
return {Status::Ok(), state};
|
||||
}
|
||||
auto it = impl_->meter_states.find(node.value);
|
||||
if (it == impl_->meter_states.end()) {
|
||||
return {Status::Error(StatusCode::kNotFound, "node not metered"), {}};
|
||||
}
|
||||
return {Status::Ok(), it->second};
|
||||
}
|
||||
|
||||
Result<MeterState> Client::MeterPeak() const {
|
||||
std::lock_guard<std::mutex> lock(impl_->cache_mutex);
|
||||
if (impl_->master_meter_data) {
|
||||
MeterState state;
|
||||
state.peak_left = impl_->master_meter_data->peak_left.load(std::memory_order_relaxed);
|
||||
state.peak_right = impl_->master_meter_data->peak_right.load(std::memory_order_relaxed);
|
||||
return {Status::Ok(), state};
|
||||
}
|
||||
return {Status::Ok(), impl_->master_meter};
|
||||
}
|
||||
|
||||
Result<Link> Client::CreateLink(PortId output, PortId input, const LinkOptions& options) {
|
||||
Status status = impl_->EnsureConnected();
|
||||
if (!status.ok()) {
|
||||
|
|
@ -1805,6 +2070,38 @@ Result<VolumeState> Client::Test_GetNodeVolume(NodeId node) const {
|
|||
}
|
||||
return {Status::Ok(), it->second};
|
||||
}
|
||||
|
||||
Status Client::Test_SetNodeMeterPeak(NodeId node, float left, float right) {
|
||||
if (!impl_) {
|
||||
return Status::Error(StatusCode::kUnavailable, "no impl");
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(impl_->cache_mutex);
|
||||
float cl = std::clamp(left, 0.0f, 1.0f);
|
||||
float cr = std::clamp(right, 0.0f, 1.0f);
|
||||
impl_->meter_states[node.value] = MeterState{cl, cr};
|
||||
impl_->metered_nodes.insert(node.value);
|
||||
auto it = impl_->live_meters.find(node.value);
|
||||
if (it != impl_->live_meters.end() && it->second) {
|
||||
it->second->peak_left.store(cl, std::memory_order_relaxed);
|
||||
it->second->peak_right.store(cr, std::memory_order_relaxed);
|
||||
}
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
Status Client::Test_SetMasterMeterPeak(float left, float right) {
|
||||
if (!impl_) {
|
||||
return Status::Error(StatusCode::kUnavailable, "no impl");
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(impl_->cache_mutex);
|
||||
float cl = std::clamp(left, 0.0f, 1.0f);
|
||||
float cr = std::clamp(right, 0.0f, 1.0f);
|
||||
impl_->master_meter = MeterState{cl, cr};
|
||||
if (impl_->master_meter_data) {
|
||||
impl_->master_meter_data->peak_left.store(cl, std::memory_order_relaxed);
|
||||
impl_->master_meter_data->peak_right.store(cr, std::memory_order_relaxed);
|
||||
}
|
||||
return Status::Ok();
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace warppipe
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include <warppipe/warppipe.hpp>
|
||||
|
||||
#include "../../gui/AudioLevelMeter.h"
|
||||
#include "../../gui/GraphEditorWidget.h"
|
||||
#include "../../gui/PresetManager.h"
|
||||
#include "../../gui/VolumeWidgets.h"
|
||||
|
|
@ -12,6 +13,7 @@
|
|||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QStandardPaths>
|
||||
#include <QTabWidget>
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <catch2/catch_approx.hpp>
|
||||
|
|
@ -1151,6 +1153,81 @@ TEST_CASE("preset saves and loads volume state") {
|
|||
QFile::remove(path);
|
||||
}
|
||||
|
||||
TEST_CASE("AudioLevelMeter setLevel clamps to 0-1") {
|
||||
ensureApp();
|
||||
AudioLevelMeter meter;
|
||||
meter.setLevel(0.5f);
|
||||
REQUIRE(meter.level() == Catch::Approx(0.5f));
|
||||
meter.setLevel(-0.5f);
|
||||
REQUIRE(meter.level() == Catch::Approx(0.0f));
|
||||
meter.setLevel(1.5f);
|
||||
REQUIRE(meter.level() == Catch::Approx(1.0f));
|
||||
}
|
||||
|
||||
TEST_CASE("AudioLevelMeter peak hold tracks maximum") {
|
||||
ensureApp();
|
||||
AudioLevelMeter meter;
|
||||
meter.setLevel(0.8f);
|
||||
REQUIRE(meter.peakHold() == Catch::Approx(0.8f));
|
||||
meter.setLevel(0.3f);
|
||||
REQUIRE(meter.peakHold() == Catch::Approx(0.8f));
|
||||
meter.setLevel(0.9f);
|
||||
REQUIRE(meter.peakHold() == Catch::Approx(0.9f));
|
||||
}
|
||||
|
||||
TEST_CASE("AudioLevelMeter peak decays after hold period") {
|
||||
ensureApp();
|
||||
AudioLevelMeter meter;
|
||||
meter.setLevel(0.5f);
|
||||
REQUIRE(meter.peakHold() == Catch::Approx(0.5f));
|
||||
for (int i = 0; i < 7; ++i)
|
||||
meter.setLevel(0.0f);
|
||||
REQUIRE(meter.peakHold() < 0.5f);
|
||||
REQUIRE(meter.peakHold() > 0.0f);
|
||||
}
|
||||
|
||||
TEST_CASE("AudioLevelMeter resetPeakHold clears peak") {
|
||||
ensureApp();
|
||||
AudioLevelMeter meter;
|
||||
meter.setLevel(0.7f);
|
||||
REQUIRE(meter.peakHold() == Catch::Approx(0.7f));
|
||||
meter.resetPeakHold();
|
||||
REQUIRE(meter.peakHold() == Catch::Approx(0.0f));
|
||||
}
|
||||
|
||||
TEST_CASE("GraphEditorWidget has METERS tab") {
|
||||
auto tc = TestClient::Create();
|
||||
if (!tc.available()) { SUCCEED("PipeWire unavailable"); return; }
|
||||
ensureApp();
|
||||
|
||||
GraphEditorWidget widget(tc.client.get());
|
||||
auto *sidebar = widget.findChild<QTabWidget *>();
|
||||
REQUIRE(sidebar != nullptr);
|
||||
bool found = false;
|
||||
for (int i = 0; i < sidebar->count(); ++i) {
|
||||
if (sidebar->tabText(i) == "METERS") {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
REQUIRE(found);
|
||||
}
|
||||
|
||||
TEST_CASE("node meter rows created for injected nodes") {
|
||||
auto tc = TestClient::Create();
|
||||
if (!tc.available()) { SUCCEED("PipeWire unavailable"); return; }
|
||||
ensureApp();
|
||||
|
||||
REQUIRE(tc.client->Test_InsertNode(
|
||||
MakeNode(100700, "meter-node", "Audio/Sink")).ok());
|
||||
REQUIRE(tc.client->Test_InsertPort(
|
||||
MakePort(100701, 100700, "FL", true)).ok());
|
||||
|
||||
GraphEditorWidget widget(tc.client.get());
|
||||
auto meters = widget.findChildren<AudioLevelMeter *>();
|
||||
REQUIRE(meters.size() >= 3);
|
||||
}
|
||||
|
||||
TEST_CASE("volume state cleaned up on node deletion") {
|
||||
auto tc = TestClient::Create();
|
||||
if (!tc.available()) { SUCCEED("PipeWire unavailable"); return; }
|
||||
|
|
|
|||
|
|
@ -853,3 +853,109 @@ TEST_CASE("Test_SetNodeVolume fails for nonexistent node") {
|
|||
REQUIRE_FALSE(status.ok());
|
||||
REQUIRE(status.code == warppipe::StatusCode::kNotFound);
|
||||
}
|
||||
|
||||
TEST_CASE("EnsureNodeMeter and NodeMeterPeak round-trip") {
|
||||
auto result = warppipe::Client::Create(DefaultOptions());
|
||||
REQUIRE(result.ok());
|
||||
auto &client = result.value;
|
||||
|
||||
warppipe::NodeInfo node;
|
||||
node.id = warppipe::NodeId{950};
|
||||
node.name = "meter-test";
|
||||
node.media_class = "Audio/Sink";
|
||||
REQUIRE(client->Test_InsertNode(node).ok());
|
||||
|
||||
REQUIRE(client->EnsureNodeMeter(warppipe::NodeId{950}).ok());
|
||||
|
||||
auto peak = client->NodeMeterPeak(warppipe::NodeId{950});
|
||||
REQUIRE(peak.ok());
|
||||
REQUIRE(peak.value.peak_left == Catch::Approx(0.0f));
|
||||
REQUIRE(peak.value.peak_right == Catch::Approx(0.0f));
|
||||
}
|
||||
|
||||
TEST_CASE("Test_SetNodeMeterPeak updates peaks") {
|
||||
auto result = warppipe::Client::Create(DefaultOptions());
|
||||
REQUIRE(result.ok());
|
||||
auto &client = result.value;
|
||||
|
||||
warppipe::NodeInfo node;
|
||||
node.id = warppipe::NodeId{951};
|
||||
node.name = "meter-set";
|
||||
node.media_class = "Audio/Sink";
|
||||
REQUIRE(client->Test_InsertNode(node).ok());
|
||||
|
||||
REQUIRE(client->Test_SetNodeMeterPeak(warppipe::NodeId{951}, 0.6f, 0.8f).ok());
|
||||
|
||||
auto peak = client->NodeMeterPeak(warppipe::NodeId{951});
|
||||
REQUIRE(peak.ok());
|
||||
REQUIRE(peak.value.peak_left == Catch::Approx(0.6f));
|
||||
REQUIRE(peak.value.peak_right == Catch::Approx(0.8f));
|
||||
}
|
||||
|
||||
TEST_CASE("DisableNodeMeter removes metering") {
|
||||
auto result = warppipe::Client::Create(DefaultOptions());
|
||||
REQUIRE(result.ok());
|
||||
auto &client = result.value;
|
||||
|
||||
warppipe::NodeInfo node;
|
||||
node.id = warppipe::NodeId{952};
|
||||
node.name = "meter-disable";
|
||||
node.media_class = "Audio/Sink";
|
||||
REQUIRE(client->Test_InsertNode(node).ok());
|
||||
REQUIRE(client->EnsureNodeMeter(warppipe::NodeId{952}).ok());
|
||||
REQUIRE(client->DisableNodeMeter(warppipe::NodeId{952}).ok());
|
||||
|
||||
auto peak = client->NodeMeterPeak(warppipe::NodeId{952});
|
||||
REQUIRE_FALSE(peak.ok());
|
||||
REQUIRE(peak.status.code == warppipe::StatusCode::kNotFound);
|
||||
}
|
||||
|
||||
TEST_CASE("MasterMeterPeak defaults to zero") {
|
||||
auto result = warppipe::Client::Create(DefaultOptions());
|
||||
REQUIRE(result.ok());
|
||||
|
||||
auto peak = result.value->MeterPeak();
|
||||
REQUIRE(peak.ok());
|
||||
REQUIRE(peak.value.peak_left == Catch::Approx(0.0f));
|
||||
REQUIRE(peak.value.peak_right == Catch::Approx(0.0f));
|
||||
}
|
||||
|
||||
TEST_CASE("Test_SetMasterMeterPeak updates master peaks") {
|
||||
auto result = warppipe::Client::Create(DefaultOptions());
|
||||
REQUIRE(result.ok());
|
||||
|
||||
REQUIRE(result.value->Test_SetMasterMeterPeak(0.9f, 0.7f).ok());
|
||||
|
||||
auto peak = result.value->MeterPeak();
|
||||
REQUIRE(peak.ok());
|
||||
REQUIRE(peak.value.peak_left == Catch::Approx(0.9f));
|
||||
REQUIRE(peak.value.peak_right == Catch::Approx(0.7f));
|
||||
}
|
||||
|
||||
TEST_CASE("Test_SetNodeMeterPeak clamps values") {
|
||||
auto result = warppipe::Client::Create(DefaultOptions());
|
||||
REQUIRE(result.ok());
|
||||
auto &client = result.value;
|
||||
|
||||
warppipe::NodeInfo node;
|
||||
node.id = warppipe::NodeId{953};
|
||||
node.name = "meter-clamp";
|
||||
node.media_class = "Audio/Sink";
|
||||
REQUIRE(client->Test_InsertNode(node).ok());
|
||||
|
||||
REQUIRE(client->Test_SetNodeMeterPeak(warppipe::NodeId{953}, 1.5f, -0.5f).ok());
|
||||
|
||||
auto peak = client->NodeMeterPeak(warppipe::NodeId{953});
|
||||
REQUIRE(peak.ok());
|
||||
REQUIRE(peak.value.peak_left == Catch::Approx(1.0f));
|
||||
REQUIRE(peak.value.peak_right == Catch::Approx(0.0f));
|
||||
}
|
||||
|
||||
TEST_CASE("EnsureNodeMeter fails for nonexistent node") {
|
||||
auto result = warppipe::Client::Create(DefaultOptions());
|
||||
REQUIRE(result.ok());
|
||||
|
||||
auto status = result.value->EnsureNodeMeter(warppipe::NodeId{999});
|
||||
REQUIRE_FALSE(status.ok());
|
||||
REQUIRE(status.code == warppipe::StatusCode::kNotFound);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue