Files
oplabs-mixer-app/daemon/MixerIpcServer.cpp
T
shawn d19e0ea7a8 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.
2026-06-23 12:15:09 -04:00

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()) + "\"}";
}
}