d19e0ea7a8
MasterBus: stereo master fader, mute, level meters in PiPedal theme (purple accent per mixer-engine branch reference). BusStrip: added per-channel routing selector (ch1Route..ch8Route) for selecting output bus target (Main, Bus 2-8). MixerPage: integrated MasterBus with levels derived from bus master data. Local state for volume/mute until backend exposes master output.
754 lines
23 KiB
C++
754 lines
23 KiB
C++
// Copyright (c) 2026 Ourpad Network
|
|
// See LICENSE file in the project root for full license text.
|
|
//
|
|
// MixerIpcServer — Unix domain socket IPC server implementation.
|
|
|
|
#include "MixerIpcServer.hpp"
|
|
#include "MixerApi.hpp"
|
|
#include "MixerEngine.hpp"
|
|
|
|
#include <sys/socket.h>
|
|
#include <sys/un.h>
|
|
#include <unistd.h>
|
|
#include <fcntl.h>
|
|
#include <cstring>
|
|
#include <cstdio>
|
|
#include <cerrno>
|
|
#include <sstream>
|
|
#include <algorithm>
|
|
|
|
#include <arpa/inet.h> // for htonl/ntohl
|
|
|
|
// JSON writer/reader (inline minimal implementation to avoid heavy dependency)
|
|
// In production, use a proper JSON library like nlohmann/json or boost.json.
|
|
// Here we use the PiPedal project's own json_writer/json_reader if available,
|
|
// but since this is a standalone daemon, we provide a minimal inline version.
|
|
|
|
namespace {
|
|
|
|
// Write a JSON string with proper escaping
|
|
std::string jsonEscape(const std::string& s) {
|
|
std::string out;
|
|
out.reserve(s.size() + 2);
|
|
for (char c : s) {
|
|
switch (c) {
|
|
case '"': out += "\\\""; break;
|
|
case '\\': out += "\\\\"; break;
|
|
case '\b': out += "\\b"; break;
|
|
case '\f': out += "\\f"; break;
|
|
case '\n': out += "\\n"; break;
|
|
case '\r': out += "\\r"; break;
|
|
case '\t': out += "\\t"; break;
|
|
default:
|
|
if (static_cast<unsigned char>(c) < 0x20) {
|
|
char buf[8];
|
|
snprintf(buf, sizeof(buf), "\\u%04x", c);
|
|
out += buf;
|
|
} else {
|
|
out += c;
|
|
}
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// Simple JSON object builder
|
|
class JsonBuilder {
|
|
public:
|
|
JsonBuilder() : ss_() { ss_ << "{"; first_ = true; }
|
|
|
|
void add(const std::string& key, const std::string& value) {
|
|
if (!first_) ss_ << ",";
|
|
first_ = false;
|
|
ss_ << "\"" << jsonEscape(key) << "\":\"" << jsonEscape(value) << "\"";
|
|
}
|
|
|
|
void add(const std::string& key, const char* value) {
|
|
add(key, std::string(value));
|
|
}
|
|
|
|
void add(const std::string& key, int64_t value) {
|
|
if (!first_) ss_ << ",";
|
|
first_ = false;
|
|
ss_ << "\"" << jsonEscape(key) << "\":" << value;
|
|
}
|
|
|
|
void add(const std::string& key, double value) {
|
|
if (!first_) ss_ << ",";
|
|
first_ = false;
|
|
ss_ << "\"" << jsonEscape(key) << "\":" << value;
|
|
}
|
|
|
|
void add(const std::string& key, bool value) {
|
|
if (!first_) ss_ << ",";
|
|
first_ = false;
|
|
ss_ << "\"" << jsonEscape(key) << "\":" << (value ? "true" : "false");
|
|
}
|
|
|
|
void addRaw(const std::string& key, const std::string& rawJson) {
|
|
if (!first_) ss_ << ",";
|
|
first_ = false;
|
|
ss_ << "\"" << jsonEscape(key) << "\":" << rawJson;
|
|
}
|
|
|
|
std::string build() { ss_ << "}"; return ss_.str(); }
|
|
|
|
private:
|
|
std::stringstream ss_;
|
|
bool first_ = true;
|
|
};
|
|
|
|
// Simple JSON key-value extractor (does not handle nested objects)
|
|
// Just enough for our params extraction
|
|
class SimpleJsonParser {
|
|
public:
|
|
SimpleJsonParser(const std::string& json) : data_(json), pos_(0) {
|
|
skipWhitespace();
|
|
}
|
|
|
|
bool enterObject() {
|
|
skipWhitespace();
|
|
if (pos_ < data_.size() && data_[pos_] == '{') {
|
|
pos_++;
|
|
skipWhitespace();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool enterArray() {
|
|
skipWhitespace();
|
|
if (pos_ < data_.size() && data_[pos_] == '[') {
|
|
pos_++;
|
|
skipWhitespace();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void leaveObject() {
|
|
skipWhitespace();
|
|
if (pos_ < data_.size() && data_[pos_] == '}') pos_++;
|
|
}
|
|
|
|
void leaveArray() {
|
|
skipWhitespace();
|
|
if (pos_ < data_.size() && data_[pos_] == ']') pos_++;
|
|
}
|
|
|
|
std::string readKey() {
|
|
skipWhitespace();
|
|
std::string key = readString();
|
|
skipWhitespace();
|
|
if (pos_ < data_.size() && data_[pos_] == ':') pos_++;
|
|
return key;
|
|
}
|
|
|
|
std::string readString() {
|
|
skipWhitespace();
|
|
if (pos_ >= data_.size() || data_[pos_] != '"') return "";
|
|
pos_++; // skip opening quote
|
|
std::string result;
|
|
while (pos_ < data_.size() && data_[pos_] != '"') {
|
|
if (data_[pos_] == '\\' && pos_ + 1 < data_.size()) {
|
|
pos_++;
|
|
switch (data_[pos_]) {
|
|
case '"': result += '"'; break;
|
|
case '\\': result += '\\'; break;
|
|
case '/': result += '/'; break;
|
|
case 'n': result += '\n'; break;
|
|
case 'r': result += '\r'; break;
|
|
case 't': result += '\t'; break;
|
|
default: result += data_[pos_]; break;
|
|
}
|
|
} else {
|
|
result += data_[pos_];
|
|
}
|
|
pos_++;
|
|
}
|
|
if (pos_ < data_.size() && data_[pos_] == '"') pos_++; // skip closing quote
|
|
return result;
|
|
}
|
|
|
|
int64_t readInt() {
|
|
skipWhitespace();
|
|
char* end = nullptr;
|
|
int64_t val = strtoll(data_.c_str() + pos_, &end, 10);
|
|
if (end) pos_ = end - data_.c_str();
|
|
return val;
|
|
}
|
|
|
|
double readDouble() {
|
|
skipWhitespace();
|
|
char* end = nullptr;
|
|
double val = strtod(data_.c_str() + pos_, &end);
|
|
if (end) pos_ = end - data_.c_str();
|
|
return val;
|
|
}
|
|
|
|
bool readBool() {
|
|
skipWhitespace();
|
|
if (data_.substr(pos_, 4) == "true") { pos_ += 4; return true; }
|
|
if (data_.substr(pos_, 5) == "false") { pos_ += 5; return false; }
|
|
return false;
|
|
}
|
|
|
|
std::string readRaw() {
|
|
// Read a raw JSON value (for nested objects/arrays we want to preserve)
|
|
skipWhitespace();
|
|
if (pos_ >= data_.size()) return "";
|
|
|
|
char start = data_[pos_];
|
|
if (start == '{' || start == '[') {
|
|
// Find matching close brace/bracket
|
|
char close = (start == '{') ? '}' : ']';
|
|
int depth = 0;
|
|
size_t startPos = pos_;
|
|
bool inString = false;
|
|
while (pos_ < data_.size()) {
|
|
char c = data_[pos_];
|
|
if (c == '"' && (pos_ == 0 || data_[pos_-1] != '\\')) inString = !inString;
|
|
if (!inString) {
|
|
if (c == start) depth++;
|
|
if (c == close) {
|
|
depth--;
|
|
if (depth == 0) {
|
|
pos_++;
|
|
return data_.substr(startPos, pos_ - startPos);
|
|
}
|
|
}
|
|
}
|
|
pos_++;
|
|
}
|
|
pos_ = startPos; // reset on failure
|
|
}
|
|
return "";
|
|
}
|
|
|
|
// Skip a single value (for keys we don't care about)
|
|
void skipValue() {
|
|
skipWhitespace();
|
|
if (pos_ >= data_.size()) return;
|
|
char c = data_[pos_];
|
|
if (c == '"') { readString(); }
|
|
else if (c == '{' || c == '[') { readRaw(); }
|
|
else if (c == 't' || c == 'f') { readBool(); }
|
|
else { readDouble(); } // number (also handles int)
|
|
skipWhitespace();
|
|
if (pos_ < data_.size() && (data_[pos_] == ',' || data_[pos_] == '}')) {
|
|
if (data_[pos_] == ',') pos_++;
|
|
}
|
|
}
|
|
|
|
// Check if we've reached the end of the current object
|
|
bool atEnd() {
|
|
skipWhitespace();
|
|
return pos_ >= data_.size() || data_[pos_] == '}';
|
|
}
|
|
|
|
// Parse a string value for a given key from a flat JSON object
|
|
std::string getString(const std::string& key) {
|
|
if (!enterObject()) return "";
|
|
while (!atEnd()) {
|
|
std::string k = readKey();
|
|
if (k == key) { std::string v = readString(); leaveObject(); return v; }
|
|
else { skipValue(); }
|
|
}
|
|
leaveObject();
|
|
return "";
|
|
}
|
|
|
|
int64_t getInt(const std::string& key, int64_t def = 0) {
|
|
if (!enterObject()) return def;
|
|
while (!atEnd()) {
|
|
std::string k = readKey();
|
|
if (k == key) { int64_t v = readInt(); leaveObject(); return v; }
|
|
else { skipValue(); }
|
|
}
|
|
leaveObject();
|
|
return def;
|
|
}
|
|
|
|
double getDouble(const std::string& key, double def = 0.0) {
|
|
if (!enterObject()) return def;
|
|
while (!atEnd()) {
|
|
std::string k = readKey();
|
|
if (k == key) { double v = readDouble(); leaveObject(); return v; }
|
|
else { skipValue(); }
|
|
}
|
|
leaveObject();
|
|
return def;
|
|
}
|
|
|
|
bool getBool(const std::string& key, bool def = false) {
|
|
if (!enterObject()) return def;
|
|
while (!atEnd()) {
|
|
std::string k = readKey();
|
|
if (k == key) { bool v = readBool(); leaveObject(); return v; }
|
|
else { skipValue(); }
|
|
}
|
|
leaveObject();
|
|
return def;
|
|
}
|
|
|
|
std::string getRaw(const std::string& key) {
|
|
if (!enterObject()) return "";
|
|
while (!atEnd()) {
|
|
std::string k = readKey();
|
|
if (k == key) { std::string v = readRaw(); leaveObject(); return v; }
|
|
else { skipValue(); }
|
|
}
|
|
leaveObject();
|
|
return "";
|
|
}
|
|
|
|
private:
|
|
const std::string& data_;
|
|
size_t pos_;
|
|
|
|
void skipWhitespace() {
|
|
while (pos_ < data_.size() && (data_[pos_] == ' ' || data_[pos_] == '\t' ||
|
|
data_[pos_] == '\n' || data_[pos_] == '\r'))
|
|
pos_++;
|
|
}
|
|
};
|
|
|
|
} // anonymous namespace
|
|
|
|
using namespace pipedal;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Construction / Destruction
|
|
// ---------------------------------------------------------------------------
|
|
|
|
MixerIpcServer::MixerIpcServer() {}
|
|
MixerIpcServer::~MixerIpcServer() { stop(); }
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Start / Stop
|
|
// ---------------------------------------------------------------------------
|
|
|
|
bool MixerIpcServer::start(const std::string& socketPath) {
|
|
if (running_.load()) {
|
|
return true; // already running
|
|
}
|
|
|
|
socketPath_ = socketPath;
|
|
|
|
// Remove existing socket file if present
|
|
unlink(socketPath_.c_str());
|
|
|
|
// Create socket
|
|
serverFd_ = socket(AF_UNIX, SOCK_STREAM, 0);
|
|
if (serverFd_ < 0) {
|
|
perror("socket");
|
|
return false;
|
|
}
|
|
|
|
// Bind
|
|
struct sockaddr_un addr;
|
|
memset(&addr, 0, sizeof(addr));
|
|
addr.sun_family = AF_UNIX;
|
|
strncpy(addr.sun_path, socketPath_.c_str(), sizeof(addr.sun_path) - 1);
|
|
|
|
if (bind(serverFd_, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
|
|
perror("bind");
|
|
close(serverFd_);
|
|
serverFd_ = -1;
|
|
return false;
|
|
}
|
|
|
|
// Set permissions so the FastAPI process (running as same user) can connect
|
|
chmod(socketPath_.c_str(), 0666);
|
|
|
|
// Listen
|
|
if (listen(serverFd_, 5) < 0) {
|
|
perror("listen");
|
|
close(serverFd_);
|
|
serverFd_ = -1;
|
|
unlink(socketPath_.c_str());
|
|
return false;
|
|
}
|
|
|
|
running_.store(true);
|
|
|
|
// Start accept thread
|
|
acceptThread_ = std::thread(&MixerIpcServer::acceptLoop, this);
|
|
|
|
return true;
|
|
}
|
|
|
|
void MixerIpcServer::stop() {
|
|
running_.store(false);
|
|
|
|
// Close all client connections
|
|
for (auto& [id, conn] : clients_) {
|
|
if (conn->fd >= 0) {
|
|
close(conn->fd);
|
|
conn->fd = -1;
|
|
}
|
|
}
|
|
clients_.clear();
|
|
|
|
// Close server socket
|
|
if (serverFd_ >= 0) {
|
|
close(serverFd_);
|
|
serverFd_ = -1;
|
|
}
|
|
|
|
// Remove socket file
|
|
if (!socketPath_.empty()) {
|
|
unlink(socketPath_.c_str());
|
|
}
|
|
|
|
// Join accept thread
|
|
if (acceptThread_.joinable()) {
|
|
acceptThread_.join();
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Communication
|
|
// ---------------------------------------------------------------------------
|
|
|
|
bool MixerIpcServer::sendFrame(int fd, const std::string& jsonPayload) {
|
|
if (fd < 0) return false;
|
|
|
|
uint32_t len = static_cast<uint32_t>(jsonPayload.size());
|
|
uint32_t lenBe = htonl(len);
|
|
|
|
// Write length prefix
|
|
ssize_t written = write(fd, &lenBe, sizeof(lenBe));
|
|
if (written != sizeof(lenBe)) return false;
|
|
|
|
// Write payload
|
|
written = write(fd, jsonPayload.data(), jsonPayload.size());
|
|
if (written != static_cast<ssize_t>(jsonPayload.size())) return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool MixerIpcServer::sendResponse(int clientFd, const std::string& jsonResponse) {
|
|
return sendFrame(clientFd, jsonResponse);
|
|
}
|
|
|
|
void MixerIpcServer::broadcast(const std::string& jsonMessage) {
|
|
auto fds = connectedClientFds();
|
|
for (int fd : fds) {
|
|
if (fd >= 0) {
|
|
sendFrame(fd, jsonMessage);
|
|
}
|
|
}
|
|
}
|
|
|
|
std::vector<int> MixerIpcServer::connectedClientFds() const {
|
|
std::vector<int> fds;
|
|
for (const auto& [id, conn] : clients_) {
|
|
if (conn->active && conn->fd >= 0) {
|
|
fds.push_back(conn->fd);
|
|
}
|
|
}
|
|
return fds;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Accept loop
|
|
// ---------------------------------------------------------------------------
|
|
|
|
void MixerIpcServer::acceptLoop() {
|
|
while (running_.load()) {
|
|
struct sockaddr_un clientAddr;
|
|
socklen_t clientLen = sizeof(clientAddr);
|
|
|
|
int clientFd = accept(serverFd_, (struct sockaddr*)&clientAddr, &clientLen);
|
|
if (clientFd < 0) {
|
|
if (errno == EINTR) continue;
|
|
if (!running_.load()) break;
|
|
perror("accept");
|
|
continue;
|
|
}
|
|
|
|
// Create client connection
|
|
auto conn = std::make_unique<ClientConnection>();
|
|
conn->fd = clientFd;
|
|
conn->active = true;
|
|
|
|
int clientId = nextClientId_++;
|
|
clients_[clientId] = std::move(conn);
|
|
|
|
// Handle this client (blocking per-client)
|
|
// In a real implementation, this would be in a separate thread or use event loop.
|
|
// For this embedded version, we handle one client at a time per thread.
|
|
handleClient(clientId);
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Client handler
|
|
// ---------------------------------------------------------------------------
|
|
|
|
void MixerIpcServer::handleClient(int clientId) {
|
|
auto it = clients_.find(clientId);
|
|
if (it == clients_.end()) return;
|
|
|
|
auto& conn = it->second;
|
|
|
|
while (running_.load() && conn->active) {
|
|
if (!readFromClient(*conn)) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Cleanup
|
|
if (conn->fd >= 0) {
|
|
close(conn->fd);
|
|
conn->fd = -1;
|
|
}
|
|
conn->active = false;
|
|
clients_.erase(clientId);
|
|
}
|
|
|
|
bool MixerIpcServer::readFromClient(ClientConnection& conn) {
|
|
uint8_t buffer[4096];
|
|
|
|
ssize_t n = read(conn.fd, buffer, sizeof(buffer));
|
|
if (n <= 0) {
|
|
if (n == 0) {
|
|
// Connection closed
|
|
return false;
|
|
}
|
|
if (errno == EAGAIN || errno == EWOULDBLOCK) {
|
|
// No data available yet — yield and retry on next loop iteration
|
|
// In a proper impl, use poll()/epoll()
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Append to read buffer
|
|
conn.readBuffer.insert(conn.readBuffer.end(), buffer, buffer + n);
|
|
|
|
// Process complete frames
|
|
while (true) {
|
|
if (conn.readingHeader) {
|
|
if (conn.readBuffer.size() < 4) break;
|
|
|
|
// Read length prefix (big-endian uint32)
|
|
uint32_t lenBe;
|
|
memcpy(&lenBe, conn.readBuffer.data(), 4);
|
|
conn.expectedLength = ntohl(lenBe);
|
|
|
|
// Remove header from buffer
|
|
conn.readBuffer.erase(conn.readBuffer.begin(), conn.readBuffer.begin() + 4);
|
|
|
|
conn.readingHeader = false;
|
|
}
|
|
|
|
if (conn.readBuffer.size() < conn.expectedLength) break;
|
|
|
|
// We have a complete message
|
|
std::string jsonMessage(conn.readBuffer.begin(),
|
|
conn.readBuffer.begin() + conn.expectedLength);
|
|
conn.readBuffer.erase(conn.readBuffer.begin(),
|
|
conn.readBuffer.begin() + conn.expectedLength);
|
|
|
|
conn.readingHeader = true;
|
|
conn.expectedLength = 0;
|
|
|
|
// Process the message
|
|
processMessage(conn, jsonMessage);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Message processing
|
|
// ---------------------------------------------------------------------------
|
|
|
|
void MixerIpcServer::processMessage(ClientConnection& conn, const std::string& jsonMessage) {
|
|
if (messageCallback_) {
|
|
messageCallback_(jsonMessage);
|
|
}
|
|
|
|
// Parse the JSON message
|
|
SimpleJsonParser parser(jsonMessage);
|
|
|
|
// Extract id, method, params
|
|
int64_t msgId = 0;
|
|
std::string method;
|
|
std::string params;
|
|
|
|
if (!parser.enterObject()) {
|
|
// Invalid message
|
|
JsonBuilder err;
|
|
err.add("id", 0);
|
|
err.add("error", std::string("Invalid JSON: expected object"));
|
|
sendResponse(conn.fd, err.build());
|
|
return;
|
|
}
|
|
|
|
while (!parser.atEnd()) {
|
|
std::string key = parser.readKey();
|
|
if (key == "id") {
|
|
msgId = parser.readInt();
|
|
} else if (key == "method") {
|
|
method = parser.readString();
|
|
} else if (key == "params") {
|
|
// Read raw params value (could be object, array, etc.)
|
|
params = parser.readRaw();
|
|
} else {
|
|
parser.skipValue();
|
|
}
|
|
}
|
|
parser.leaveObject();
|
|
|
|
if (method.empty()) {
|
|
JsonBuilder err;
|
|
err.add("id", msgId);
|
|
err.add("error", std::string("Missing 'method' field"));
|
|
sendResponse(conn.fd, err.build());
|
|
return;
|
|
}
|
|
|
|
// Dispatch and get response
|
|
std::string resultJson = dispatchMethod(method, params);
|
|
|
|
// Wrap with response id
|
|
std::stringstream ss;
|
|
ss << "{\"id\":" << msgId << ",";
|
|
// The resultJson starts with {"result": ...} or {"error": ...}
|
|
// Strip the outer { and add our id
|
|
if (!resultJson.empty() && resultJson[0] == '{') {
|
|
ss << resultJson.substr(1); // skip leading {
|
|
} else {
|
|
ss << "\"result\":" << resultJson << ",\"error\":null";
|
|
}
|
|
|
|
sendResponse(conn.fd, ss.str());
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Method dispatch
|
|
// ---------------------------------------------------------------------------
|
|
|
|
std::string MixerIpcServer::dispatchMethod(const std::string& method, const std::string& paramsJson) {
|
|
if (!mixerApi_) {
|
|
return "{\"error\":\"no mixer api configured\"}";
|
|
}
|
|
|
|
try {
|
|
if (method == "getState") {
|
|
std::string state = mixerApi_->getStateJson();
|
|
return "{\"result\":" + state + ",\"error\":null}";
|
|
}
|
|
else if (method == "setChannelVolume") {
|
|
SimpleJsonParser p(paramsJson);
|
|
int ci = (int)p.getInt("channelIndex", -1);
|
|
double vol = p.getDouble("volumeDb", -96.0);
|
|
if (ci < 0) return "{\"error\":\"missing channelIndex\"}";
|
|
mixerApi_->setChannelVolume(ci, (float)vol);
|
|
return "{\"result\":{},\"error\":null}";
|
|
}
|
|
else if (method == "setChannelPan") {
|
|
SimpleJsonParser p(paramsJson);
|
|
int ci = (int)p.getInt("channelIndex", -1);
|
|
double pan = p.getDouble("pan", 0.0);
|
|
if (ci < 0) return "{\"error\":\"missing channelIndex\"}";
|
|
mixerApi_->setChannelPan(ci, (float)pan);
|
|
return "{\"result\":{},\"error\":null}";
|
|
}
|
|
else if (method == "setChannelMute") {
|
|
SimpleJsonParser p(paramsJson);
|
|
int ci = (int)p.getInt("channelIndex", -1);
|
|
bool mute = p.getBool("mute", false);
|
|
if (ci < 0) return "{\"error\":\"missing channelIndex\"}";
|
|
mixerApi_->setChannelMute(ci, mute);
|
|
return "{\"result\":{},\"error\":null}";
|
|
}
|
|
else if (method == "setChannelSolo") {
|
|
SimpleJsonParser p(paramsJson);
|
|
int ci = (int)p.getInt("channelIndex", -1);
|
|
bool solo = p.getBool("solo", false);
|
|
if (ci < 0) return "{\"error\":\"missing channelIndex\"}";
|
|
mixerApi_->setChannelSolo(ci, solo);
|
|
return "{\"result\":{},\"error\":null}";
|
|
}
|
|
else if (method == "addChannel") {
|
|
SimpleJsonParser p(paramsJson);
|
|
int pi = (int)p.getInt("physicalInputIndex", 0);
|
|
int idx = mixerApi_->addChannel(pi);
|
|
JsonBuilder resp;
|
|
resp.add("channelIndex", (int64_t)idx);
|
|
return "{\"result\":" + resp.build() + ",\"error\":null}";
|
|
}
|
|
else if (method == "removeChannel") {
|
|
SimpleJsonParser p(paramsJson);
|
|
int ci = (int)p.getInt("channelIndex", -1);
|
|
if (ci < 0) return "{\"error\":\"missing channelIndex\"}";
|
|
mixerApi_->removeChannel(ci);
|
|
return "{\"result\":{},\"error\":null}";
|
|
}
|
|
else if (method == "getOutputRoutes") {
|
|
std::string routes = mixerApi_->getOutputRoutesJson();
|
|
return "{\"result\":{\"routes\":" + routes + "},\"error\":null}";
|
|
}
|
|
else if (method == "setOutputRoutes") {
|
|
// params is the raw JSON for output routes array
|
|
// We need the routes array from params
|
|
SimpleJsonParser p(paramsJson);
|
|
std::string routesArray = p.getRaw("routes");
|
|
if (!routesArray.empty()) {
|
|
mixerApi_->setOutputRoutesFromJson(routesArray);
|
|
}
|
|
return "{\"result\":{},\"error\":null}";
|
|
}
|
|
else if (method == "saveScene") {
|
|
SimpleJsonParser p(paramsJson);
|
|
std::string name = p.getString("name");
|
|
if (name.empty()) return "{\"error\":\"missing name\"}";
|
|
std::string result = mixerApi_->saveScene(name);
|
|
// result from MixerApi is already JSON: {"id": N, "name": "..."}
|
|
return "{\"result\":" + result + ",\"error\":null}";
|
|
}
|
|
else if (method == "loadScene") {
|
|
SimpleJsonParser p(paramsJson);
|
|
std::string sceneId = p.getString("sceneId");
|
|
if (sceneId.empty()) {
|
|
// Try as integer
|
|
int64_t id = p.getInt("sceneId", -1);
|
|
if (id < 0) return "{\"error\":\"missing sceneId\"}";
|
|
sceneId = std::to_string(id);
|
|
}
|
|
bool ok = mixerApi_->loadScene(sceneId);
|
|
JsonBuilder resp;
|
|
resp.add("ok", ok);
|
|
return "{\"result\":" + resp.build() + ",\"error\":null}";
|
|
}
|
|
else if (method == "listScenes") {
|
|
std::string scenes = mixerApi_->listScenes();
|
|
if (scenes.empty()) scenes = "[]";
|
|
return "{\"result\":{\"scenes\":" + scenes + "},\"error\":null}";
|
|
}
|
|
else if (method == "deleteScene") {
|
|
SimpleJsonParser p(paramsJson);
|
|
std::string sceneId = p.getString("sceneId");
|
|
if (sceneId.empty()) {
|
|
int64_t id = p.getInt("sceneId", -1);
|
|
if (id < 0) return "{\"error\":\"missing sceneId\"}";
|
|
sceneId = std::to_string(id);
|
|
}
|
|
bool ok = mixerApi_->deleteScene(sceneId);
|
|
JsonBuilder resp;
|
|
resp.add("ok", ok);
|
|
return "{\"result\":" + resp.build() + ",\"error\":null}";
|
|
}
|
|
else {
|
|
return "{\"error\":\"unknown method: " + jsonEscape(method) + "\"}";
|
|
}
|
|
}
|
|
catch (const std::exception& e) {
|
|
return "{\"error\":\"" + jsonEscape(e.what()) + "\"}";
|
|
}
|
|
}
|