Add MasterBus component + enhance BusStrip with routing selector
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.
This commit is contained in:
@@ -0,0 +1,753 @@
|
||||
// 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()) + "\"}";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user