Save presets
This commit is contained in:
parent
ecfb59501a
commit
b3a1d2b7f3
9 changed files with 542 additions and 4 deletions
|
|
@ -90,6 +90,7 @@ add_executable(potato-gui
|
|||
src/gui/GraphEditorWidget.cpp
|
||||
src/gui/PipeWireGraphModel.cpp
|
||||
src/meters/AudioLevelMeter.cpp
|
||||
src/presets/PresetManager.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(potato-gui PRIVATE
|
||||
|
|
|
|||
|
|
@ -1221,9 +1221,9 @@ private:
|
|||
### Milestone 4: Virtual Devices & State Management
|
||||
**Estimated Time:** 2 weeks
|
||||
- [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
|
||||
- [x] Create `PresetManager` with JSON serialization
|
||||
- [x] Implement preset load/save functionality
|
||||
- [x] Store UI layout alongside audio graph state
|
||||
- [ ] Implement auto-reconnect for device hotplug
|
||||
- [ ] **Acceptance Criteria:** Create virtual device, save preset, restore on restart
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "GraphEditorWidget.h"
|
||||
#include "meters/AudioLevelMeter.h"
|
||||
#include "presets/PresetManager.h"
|
||||
|
||||
#include <QAction>
|
||||
#include <QCoreApplication>
|
||||
|
|
@ -175,6 +176,38 @@ GraphEditorWidget::GraphEditorWidget(Potato::PipeWireController *controller, QWi
|
|||
});
|
||||
m_view->addAction(resetLayoutAction);
|
||||
|
||||
auto *savePresetAction = new QAction(QString("Save Preset..."), m_view);
|
||||
connect(savePresetAction, &QAction::triggered, [this]() {
|
||||
const QString filePath = QFileDialog::getSaveFileName(this,
|
||||
QString("Save Preset"),
|
||||
QString(),
|
||||
QString("Preset Files (*.json)"));
|
||||
if (filePath.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (!m_presetManager->savePreset(filePath)) {
|
||||
qWarning() << "Failed to save preset" << filePath;
|
||||
}
|
||||
});
|
||||
m_view->addAction(savePresetAction);
|
||||
|
||||
auto *loadPresetAction = new QAction(QString("Load Preset..."), m_view);
|
||||
connect(loadPresetAction, &QAction::triggered, [this]() {
|
||||
const QString filePath = QFileDialog::getOpenFileName(this,
|
||||
QString("Load Preset"),
|
||||
QString(),
|
||||
QString("Preset Files (*.json)"));
|
||||
if (filePath.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (!m_presetManager->loadPreset(filePath)) {
|
||||
qWarning() << "Failed to load preset" << filePath;
|
||||
return;
|
||||
}
|
||||
reloadGraphFromController();
|
||||
});
|
||||
m_view->addAction(loadPresetAction);
|
||||
|
||||
auto *createVirtualSinkAction = new QAction(QString("Create Virtual Sink"), m_view);
|
||||
connect(createVirtualSinkAction, &QAction::triggered, [this]() {
|
||||
const int index = ++m_virtualSinkCount;
|
||||
|
|
@ -233,6 +266,8 @@ GraphEditorWidget::GraphEditorWidget(Potato::PipeWireController *controller, QWi
|
|||
|
||||
m_meterProfileTimer.start();
|
||||
m_meterProfileReady = true;
|
||||
|
||||
m_presetManager = new PresetManager(m_controller, m_model, this);
|
||||
}
|
||||
|
||||
static bool isAudioEndpoint(const Potato::NodeInfo &node)
|
||||
|
|
@ -508,6 +543,28 @@ void GraphEditorWidget::updateLayoutState()
|
|||
}
|
||||
}
|
||||
|
||||
void GraphEditorWidget::reloadGraphFromController()
|
||||
{
|
||||
m_ignoreCreate.clear();
|
||||
m_ignoreDelete.clear();
|
||||
m_connectionToLinkId.clear();
|
||||
m_linkIdToConnection.clear();
|
||||
m_nodeLinkCounts.clear();
|
||||
m_linksById.clear();
|
||||
|
||||
m_model->reset();
|
||||
syncGraph();
|
||||
|
||||
double viewScale = 1.0;
|
||||
QPointF viewCenter;
|
||||
if (m_model->viewState(viewScale, viewCenter)) {
|
||||
m_view->setupScale(viewScale);
|
||||
m_view->centerOn(viewCenter);
|
||||
} else {
|
||||
m_view->zoomFitAll();
|
||||
}
|
||||
}
|
||||
|
||||
void GraphEditorWidget::refreshNodeMeter(uint32_t nodeId, const Potato::NodeInfo &node)
|
||||
{
|
||||
if (m_nodeMeterRows.contains(nodeId)) {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ class QTimer;
|
|||
class QScrollArea;
|
||||
class QVBoxLayout;
|
||||
class QSplitter;
|
||||
class PresetManager;
|
||||
|
||||
class GraphEditorWidget : public QWidget
|
||||
{
|
||||
|
|
@ -46,6 +47,7 @@ private:
|
|||
void handleLinkRemoved(uint32_t linkId);
|
||||
bool isMeterNode(uint32_t nodeId) const;
|
||||
int activeLinkCount(uint32_t nodeId) const;
|
||||
void reloadGraphFromController();
|
||||
QString connectionKey(const QtNodes::ConnectionId &connectionId) const;
|
||||
bool eventFilter(QObject *object, QEvent *event) override;
|
||||
|
||||
|
|
@ -77,4 +79,5 @@ private:
|
|||
qint64 m_meterProfileNanos = 0;
|
||||
qint64 m_meterProfileMax = 0;
|
||||
int m_meterProfileFrames = 0;
|
||||
PresetManager *m_presetManager = nullptr;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -144,12 +144,19 @@ QWidget *PipeWireGraphModel::nodeWidget(QtNodes::NodeId nodeId) const
|
|||
slider->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
||||
slider->setToolTip(QString("Volume"));
|
||||
|
||||
if (pipewireId != 0 && m_nodeVolumeState.contains(pipewireId)) {
|
||||
const NodeVolumeState state = m_nodeVolumeState.value(pipewireId);
|
||||
slider->setValue(static_cast<int>(state.volume * 100.0f));
|
||||
muteButton->setChecked(state.mute);
|
||||
}
|
||||
|
||||
const auto applyVolume = [this, pipewireId, slider, muteButton]() {
|
||||
if (!m_controller || pipewireId == 0) {
|
||||
return;
|
||||
}
|
||||
const float volume = static_cast<float>(slider->value()) / 100.0f;
|
||||
m_controller->setNodeVolume(pipewireId, volume, muteButton->isChecked());
|
||||
m_nodeVolumeState.insert(pipewireId, NodeVolumeState{volume, muteButton->isChecked()});
|
||||
};
|
||||
|
||||
QObject::connect(slider, &QSlider::valueChanged, widget, [applyVolume](int) { applyVolume(); });
|
||||
|
|
@ -694,6 +701,135 @@ void PipeWireGraphModel::loadLayout()
|
|||
}
|
||||
}
|
||||
|
||||
QJsonObject PipeWireGraphModel::layoutJson() const
|
||||
{
|
||||
QJsonObject root;
|
||||
QJsonArray nodes;
|
||||
for (auto it = m_layoutByStableId.cbegin(); it != m_layoutByStableId.cend(); ++it) {
|
||||
QJsonObject item;
|
||||
item["id"] = it.key();
|
||||
item["x"] = it.value().x();
|
||||
item["y"] = it.value().y();
|
||||
nodes.append(item);
|
||||
}
|
||||
root["nodes"] = nodes;
|
||||
|
||||
QJsonObject view;
|
||||
view["scale"] = m_viewScale;
|
||||
view["center_x"] = m_viewCenter.x();
|
||||
view["center_y"] = m_viewCenter.y();
|
||||
root["view"] = view;
|
||||
|
||||
if (m_hasSplitterSizes && !m_splitterSizes.isEmpty()) {
|
||||
QJsonArray splitter;
|
||||
for (const auto size : m_splitterSizes) {
|
||||
splitter.append(size);
|
||||
}
|
||||
root["splitter"] = splitter;
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
void PipeWireGraphModel::applyLayoutJson(const QJsonObject &root)
|
||||
{
|
||||
m_layoutByStableId.clear();
|
||||
m_hasViewState = false;
|
||||
m_hasSplitterSizes = false;
|
||||
m_splitterSizes.clear();
|
||||
|
||||
const QJsonArray nodes = root.value("nodes").toArray();
|
||||
applyLayoutData(nodes);
|
||||
|
||||
const QJsonObject view = root.value("view").toObject();
|
||||
if (!view.isEmpty()) {
|
||||
m_viewScale = view.value("scale").toDouble(1.0);
|
||||
const double x = view.value("center_x").toDouble(0.0);
|
||||
const double y = view.value("center_y").toDouble(0.0);
|
||||
m_viewCenter = QPointF(x, y);
|
||||
m_hasViewState = true;
|
||||
}
|
||||
|
||||
const QJsonArray splitter = root.value("splitter").toArray();
|
||||
if (!splitter.isEmpty()) {
|
||||
QList<int> sizes;
|
||||
sizes.reserve(splitter.size());
|
||||
for (const auto &value : splitter) {
|
||||
sizes.append(value.toInt());
|
||||
}
|
||||
if (!sizes.isEmpty()) {
|
||||
m_splitterSizes = sizes;
|
||||
m_hasSplitterSizes = true;
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto &entry : m_nodes) {
|
||||
const QtNodes::NodeId nodeId = entry.first;
|
||||
const Potato::NodeInfo &info = entry.second;
|
||||
if (!info.stableId.isEmpty() && m_layoutByStableId.contains(info.stableId)) {
|
||||
const QPointF position = m_layoutByStableId.value(info.stableId);
|
||||
m_positions[nodeId] = position;
|
||||
Q_EMIT nodePositionUpdated(nodeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QHash<QString, NodeVolumeState> PipeWireGraphModel::volumeStates() const
|
||||
{
|
||||
QHash<QString, NodeVolumeState> result;
|
||||
for (const auto &entry : m_nodes) {
|
||||
const Potato::NodeInfo &info = entry.second;
|
||||
if (info.stableId.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
if (!m_nodeVolumeState.contains(info.id)) {
|
||||
continue;
|
||||
}
|
||||
result.insert(info.stableId, m_nodeVolumeState.value(info.id));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void PipeWireGraphModel::applyVolumeStates(const QHash<QString, NodeVolumeState> &states)
|
||||
{
|
||||
if (!m_controller) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QVector<Potato::NodeInfo> nodes = m_controller->nodes();
|
||||
for (const auto &node : nodes) {
|
||||
if (node.stableId.isEmpty() || !states.contains(node.stableId)) {
|
||||
continue;
|
||||
}
|
||||
const NodeVolumeState state = states.value(node.stableId);
|
||||
m_nodeVolumeState.insert(node.id, state);
|
||||
m_controller->setNodeVolume(node.id, state.volume, state.mute);
|
||||
|
||||
auto nodeIt = m_pwToNode.find(node.id);
|
||||
if (nodeIt == m_pwToNode.end()) {
|
||||
continue;
|
||||
}
|
||||
auto widgetIt = m_nodeWidgets.find(nodeIt->second);
|
||||
if (widgetIt == m_nodeWidgets.end()) {
|
||||
continue;
|
||||
}
|
||||
QWidget *widget = widgetIt->second;
|
||||
if (!widget) {
|
||||
continue;
|
||||
}
|
||||
if (auto *slider = widget->findChild<QSlider*>()) {
|
||||
slider->blockSignals(true);
|
||||
slider->setValue(static_cast<int>(state.volume * 100.0f));
|
||||
slider->blockSignals(false);
|
||||
}
|
||||
if (auto *button = widget->findChild<QToolButton*>()) {
|
||||
button->blockSignals(true);
|
||||
button->setChecked(state.mute);
|
||||
button->blockSignals(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PipeWireGraphModel::saveLayout() const
|
||||
{
|
||||
const QString path = layoutFilePath();
|
||||
|
|
|
|||
|
|
@ -18,6 +18,11 @@
|
|||
|
||||
class QWidget;
|
||||
|
||||
struct NodeVolumeState {
|
||||
float volume = 1.0f;
|
||||
bool mute = false;
|
||||
};
|
||||
|
||||
class PipeWireGraphModel : public QtNodes::AbstractGraphModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
|
@ -72,6 +77,10 @@ public:
|
|||
bool viewState(double &scale, QPointF ¢er) const;
|
||||
void setSplitterSizes(const QList<int> &sizes);
|
||||
bool splitterSizes(QList<int> &sizes) const;
|
||||
QJsonObject layoutJson() const;
|
||||
void applyLayoutJson(const QJsonObject &root);
|
||||
QHash<QString, NodeVolumeState> volumeStates() const;
|
||||
void applyVolumeStates(const QHash<QString, NodeVolumeState> &states);
|
||||
|
||||
private:
|
||||
QWidget *nodeWidget(QtNodes::NodeId nodeId) const;
|
||||
|
|
@ -100,4 +109,5 @@ private:
|
|||
bool m_hasSplitterSizes = false;
|
||||
mutable std::unordered_map<QtNodes::NodeId, QWidget*> m_nodeWidgets;
|
||||
std::unordered_map<QtNodes::NodeId, QSize> m_nodeSizes;
|
||||
mutable QHash<uint32_t, NodeVolumeState> m_nodeVolumeState;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -545,7 +545,7 @@ bool PipeWireController::createVirtualSink(const QString &name, const QString &d
|
|||
|
||||
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);
|
||||
return createVirtualDevice(name, description, "support.null-audio-sink", "Audio/Source", channels, rate);
|
||||
}
|
||||
|
||||
float PipeWireController::nodeMeterPeak(uint32_t nodeId) const
|
||||
|
|
|
|||
293
src/presets/PresetManager.cpp
Normal file
293
src/presets/PresetManager.cpp
Normal file
|
|
@ -0,0 +1,293 @@
|
|||
#include "presets/PresetManager.h"
|
||||
|
||||
#include "pipewire/pipewirecontroller.h"
|
||||
#include "gui/PipeWireGraphModel.h"
|
||||
|
||||
#include <QFile>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QVector>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
PresetManager::PresetManager(Potato::PipeWireController *controller,
|
||||
PipeWireGraphModel *model,
|
||||
QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_controller(controller)
|
||||
, m_model(model)
|
||||
{
|
||||
}
|
||||
|
||||
bool PresetManager::savePreset(const QString &path) const
|
||||
{
|
||||
if (!m_controller || !m_model || path.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const QJsonObject root = buildPreset();
|
||||
const QJsonDocument doc(root);
|
||||
|
||||
QFile file(path);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
file.write(doc.toJson(QJsonDocument::Indented));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PresetManager::loadPreset(const QString &path)
|
||||
{
|
||||
if (!m_controller || !m_model || path.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QFile file(path);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
|
||||
if (!doc.isObject()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return applyPreset(doc.object());
|
||||
}
|
||||
|
||||
QJsonObject PresetManager::buildPreset() const
|
||||
{
|
||||
QJsonObject root;
|
||||
root["version"] = QString("1.0");
|
||||
|
||||
const QVector<Potato::NodeInfo> nodes = m_controller->nodes();
|
||||
QHash<uint32_t, Potato::NodeInfo> nodesById;
|
||||
for (const auto &node : nodes) {
|
||||
nodesById.insert(node.id, node);
|
||||
}
|
||||
|
||||
QJsonArray virtualDevices;
|
||||
for (const auto &node : nodes) {
|
||||
if (node.type != Potato::NodeType::Virtual) {
|
||||
continue;
|
||||
}
|
||||
QJsonObject device;
|
||||
device["name"] = node.name;
|
||||
device["description"] = node.description;
|
||||
device["stable_id"] = node.stableId;
|
||||
const int channels = std::max(node.inputPorts.size(), node.outputPorts.size());
|
||||
device["channels"] = channels > 0 ? channels : 2;
|
||||
device["rate"] = 48000;
|
||||
device["media_class"] = mediaClassToString(node.mediaClass);
|
||||
virtualDevices.append(device);
|
||||
}
|
||||
root["virtual_devices"] = virtualDevices;
|
||||
|
||||
QJsonArray routing;
|
||||
const QVector<Potato::LinkInfo> links = m_controller->links();
|
||||
for (const auto &link : links) {
|
||||
if (!nodesById.contains(link.outputNodeId) || !nodesById.contains(link.inputNodeId)) {
|
||||
continue;
|
||||
}
|
||||
const Potato::NodeInfo &outNode = nodesById.value(link.outputNodeId);
|
||||
const Potato::NodeInfo &inNode = nodesById.value(link.inputNodeId);
|
||||
|
||||
QString outPortName;
|
||||
QString inPortName;
|
||||
if (!findPortName(outNode, link.outputPortId, outPortName)) {
|
||||
continue;
|
||||
}
|
||||
if (!findPortName(inNode, link.inputPortId, inPortName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QJsonObject route;
|
||||
route["source"] = QString("%1:%2").arg(outNode.stableId, outPortName);
|
||||
route["target"] = QString("%1:%2").arg(inNode.stableId, inPortName);
|
||||
route["volume"] = 1.0;
|
||||
route["muted"] = false;
|
||||
routing.append(route);
|
||||
}
|
||||
root["routing"] = routing;
|
||||
|
||||
QJsonObject volumes;
|
||||
QJsonObject mutes;
|
||||
const QHash<QString, NodeVolumeState> volumeStates = m_model->volumeStates();
|
||||
for (auto it = volumeStates.cbegin(); it != volumeStates.cend(); ++it) {
|
||||
volumes[it.key()] = it.value().volume;
|
||||
mutes[it.key()] = it.value().mute;
|
||||
}
|
||||
root["persistent_volumes"] = volumes;
|
||||
root["persistent_mutes"] = mutes;
|
||||
|
||||
root["ui_layout"] = m_model->layoutJson();
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
bool PresetManager::applyPreset(const QJsonObject &root)
|
||||
{
|
||||
const QVector<Potato::NodeInfo> nodes = m_controller->nodes();
|
||||
QHash<QString, Potato::NodeInfo> nodesByStableId;
|
||||
for (const auto &node : nodes) {
|
||||
if (!node.stableId.isEmpty()) {
|
||||
nodesByStableId.insert(node.stableId, node);
|
||||
}
|
||||
}
|
||||
|
||||
const QJsonArray virtualDevices = root.value("virtual_devices").toArray();
|
||||
for (const auto &entry : virtualDevices) {
|
||||
const QJsonObject device = entry.toObject();
|
||||
const QString stableId = device.value("stable_id").toString();
|
||||
const QString name = device.value("name").toString();
|
||||
const QString description = device.value("description").toString();
|
||||
const QString mediaClassValue = device.value("media_class").toString();
|
||||
const int channels = device.value("channels").toInt(2);
|
||||
const int rate = device.value("rate").toInt(48000);
|
||||
|
||||
if (!stableId.isEmpty() && nodesByStableId.contains(stableId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const Potato::MediaClass mediaClass = mediaClassFromString(mediaClassValue);
|
||||
if (mediaClass == Potato::MediaClass::AudioSource) {
|
||||
m_controller->createVirtualSource(name, description, channels, rate);
|
||||
} else {
|
||||
m_controller->createVirtualSink(name, description, channels, rate);
|
||||
}
|
||||
}
|
||||
|
||||
const QJsonArray routing = root.value("routing").toArray();
|
||||
for (const auto &entry : routing) {
|
||||
const QJsonObject route = entry.toObject();
|
||||
const QString source = route.value("source").toString();
|
||||
const QString target = route.value("target").toString();
|
||||
|
||||
uint32_t outNodeId = 0;
|
||||
uint32_t outPortId = 0;
|
||||
uint32_t inNodeId = 0;
|
||||
uint32_t inPortId = 0;
|
||||
|
||||
if (!parsePortKey(source, outNodeId, outPortId)) {
|
||||
continue;
|
||||
}
|
||||
if (!parsePortKey(target, inNodeId, inPortId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
m_controller->createLink(outNodeId, outPortId, inNodeId, inPortId);
|
||||
}
|
||||
|
||||
QHash<QString, NodeVolumeState> volumeStates;
|
||||
const QJsonObject volumes = root.value("persistent_volumes").toObject();
|
||||
const QJsonObject mutes = root.value("persistent_mutes").toObject();
|
||||
for (auto it = volumes.begin(); it != volumes.end(); ++it) {
|
||||
NodeVolumeState state;
|
||||
state.volume = static_cast<float>(it.value().toDouble(1.0));
|
||||
state.mute = mutes.value(it.key()).toBool(false);
|
||||
volumeStates.insert(it.key(), state);
|
||||
}
|
||||
if (!volumeStates.isEmpty()) {
|
||||
m_model->applyVolumeStates(volumeStates);
|
||||
}
|
||||
|
||||
const QJsonObject layout = root.value("ui_layout").toObject();
|
||||
if (!layout.isEmpty()) {
|
||||
m_model->applyLayoutJson(layout);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PresetManager::findPortName(const Potato::NodeInfo &node, uint32_t portId, QString &portName) const
|
||||
{
|
||||
for (const auto &port : node.inputPorts) {
|
||||
if (port.id == portId) {
|
||||
portName = port.name;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (const auto &port : node.outputPorts) {
|
||||
if (port.id == portId) {
|
||||
portName = port.name;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PresetManager::parsePortKey(const QString &key, uint32_t &nodeId, uint32_t &portId) const
|
||||
{
|
||||
const int split = key.lastIndexOf(':');
|
||||
if (split <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const QString stableId = key.left(split);
|
||||
const QString portName = key.mid(split + 1);
|
||||
if (stableId.isEmpty() || portName.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const QVector<Potato::NodeInfo> nodes = m_controller->nodes();
|
||||
for (const auto &node : nodes) {
|
||||
if (node.stableId != stableId) {
|
||||
continue;
|
||||
}
|
||||
for (const auto &port : node.inputPorts) {
|
||||
if (port.name == portName) {
|
||||
nodeId = node.id;
|
||||
portId = port.id;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (const auto &port : node.outputPorts) {
|
||||
if (port.name == portName) {
|
||||
nodeId = node.id;
|
||||
portId = port.id;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
QString PresetManager::portKey(const Potato::NodeInfo &node, const Potato::PortInfo &port) const
|
||||
{
|
||||
return QString("%1:%2").arg(node.stableId, port.name);
|
||||
}
|
||||
|
||||
QString PresetManager::mediaClassToString(Potato::MediaClass mediaClass) const
|
||||
{
|
||||
switch (mediaClass) {
|
||||
case Potato::MediaClass::AudioSink:
|
||||
return QString("Audio/Sink");
|
||||
case Potato::MediaClass::AudioSource:
|
||||
return QString("Audio/Source");
|
||||
case Potato::MediaClass::AudioDuplex:
|
||||
return QString("Audio/Duplex");
|
||||
case Potato::MediaClass::Stream:
|
||||
return QString("Stream");
|
||||
default:
|
||||
return QString();
|
||||
}
|
||||
}
|
||||
|
||||
Potato::MediaClass PresetManager::mediaClassFromString(const QString &value) const
|
||||
{
|
||||
if (value.contains("Audio/Source")) {
|
||||
return Potato::MediaClass::AudioSource;
|
||||
}
|
||||
if (value.contains("Audio/Duplex")) {
|
||||
return Potato::MediaClass::AudioDuplex;
|
||||
}
|
||||
if (value.contains("Audio/Sink")) {
|
||||
return Potato::MediaClass::AudioSink;
|
||||
}
|
||||
if (value.contains("Stream")) {
|
||||
return Potato::MediaClass::Stream;
|
||||
}
|
||||
return Potato::MediaClass::Unknown;
|
||||
}
|
||||
38
src/presets/PresetManager.h
Normal file
38
src/presets/PresetManager.h
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QHash>
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
#include <cstdint>
|
||||
|
||||
#include "pipewire/nodeinfo.h"
|
||||
|
||||
class PipeWireGraphModel;
|
||||
|
||||
namespace Potato {
|
||||
class PipeWireController;
|
||||
}
|
||||
|
||||
class PresetManager : public QObject
|
||||
{
|
||||
public:
|
||||
explicit PresetManager(Potato::PipeWireController *controller,
|
||||
PipeWireGraphModel *model,
|
||||
QObject *parent = nullptr);
|
||||
|
||||
bool savePreset(const QString &path) const;
|
||||
bool loadPreset(const QString &path);
|
||||
|
||||
private:
|
||||
QJsonObject buildPreset() const;
|
||||
bool applyPreset(const QJsonObject &root);
|
||||
bool findPortName(const Potato::NodeInfo &node, uint32_t portId, QString &portName) const;
|
||||
bool parsePortKey(const QString &key, uint32_t &nodeId, uint32_t &portId) const;
|
||||
QString portKey(const Potato::NodeInfo &node, const Potato::PortInfo &port) const;
|
||||
QString mediaClassToString(Potato::MediaClass mediaClass) const;
|
||||
Potato::MediaClass mediaClassFromString(const QString &value) const;
|
||||
|
||||
Potato::PipeWireController *m_controller = nullptr;
|
||||
PipeWireGraphModel *m_model = nullptr;
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue