Initial extraction: C++ MixerEngine standalone daemon

Extracted from op-pedal mixer-engine branch. Core components:

- MixerEngine / MixerChannelStrip / MixerBus — cleaned of LV2/Pedalboard/IHost/MidiMapper deps
- MixerControlServer — Unix domain socket JSON control interface
- main.cpp — JACK audio I/O client with headless fallback
- CMake build — auto-fetches nlohmann/json, links JACK
- 9 core tests passing

Architecture: JACK ports → MixerEngine::process() → JACK ports,
controlled via Unix socket JSON commands.
This commit is contained in:
2026-06-23 12:17:45 -04:00
commit a5423b7f55
150 changed files with 21198 additions and 0 deletions
+173
View File
@@ -0,0 +1,173 @@
// Copyright (c) 2026 Ourpad Network
// See LICENSE file in the project root for full license text.
#include "mixer/MixerBus.hpp"
#include <cmath>
#include <algorithm>
#include <cstring>
using namespace mixer;
MixerBus::MixerBus(int64_t id, MixerBusType type, const std::string& name, int channels)
: id_(id)
, type_(type)
, name_(name)
, channelCount_(channels)
{
buffers_.resize(channels);
}
void MixerBus::setVolume(float db)
{
volume_ = std::clamp(db, -96.0f, 12.0f);
}
void MixerBus::setMute(bool mute)
{
mute_ = mute;
}
void MixerBus::allocateBuffers(size_t maxFrames)
{
maxFrames_ = maxFrames;
for (auto& buf : buffers_) {
buf.resize(maxFrames, 0.0f);
}
}
void MixerBus::clear()
{
for (auto& buf : buffers_) {
std::fill(buf.begin(), buf.end(), 0.0f);
}
}
void MixerBus::accumulate(
const float* const* source,
uint32_t frames,
float gain,
int sourceChannels)
{
uint32_t n = std::min(frames, (uint32_t)maxFrames_);
int nChannels = std::min(sourceChannels, channelCount_);
if (std::abs(gain) < 0.0001f) return;
if (std::abs(gain - 1.0f) < 0.0001f) {
// Unity gain fast path
for (int ch = 0; ch < nChannels; ++ch) {
if (ch < (int)buffers_.size() && ch < sourceChannels && source[ch]) {
float* dst = buffers_[ch].data();
const float* src = source[ch];
for (uint32_t i = 0; i < n; ++i) {
dst[i] += src[i];
}
}
}
} else {
// Scaled accumulation
for (int ch = 0; ch < nChannels; ++ch) {
if (ch < (int)buffers_.size() && ch < sourceChannels && source[ch]) {
float* dst = buffers_[ch].data();
const float* src = source[ch];
for (uint32_t i = 0; i < n; ++i) {
dst[i] += src[i] * gain;
}
}
}
}
}
void MixerBus::accumulateMono(
const float* source,
uint32_t frames,
float gain)
{
if (!source || buffers_.empty()) return;
uint32_t n = std::min(frames, (uint32_t)maxFrames_);
float* dst = buffers_[0].data();
if (std::abs(gain) < 0.0001f) return;
if (std::abs(gain - 1.0f) < 0.0001f) {
for (uint32_t i = 0; i < n; ++i) {
dst[i] += source[i];
}
} else {
for (uint32_t i = 0; i < n; ++i) {
dst[i] += source[i] * gain;
}
}
}
void MixerBus::process(uint32_t frames)
{
uint32_t n = std::min(frames, (uint32_t)maxFrames_);
bool isMuted = mute_.load();
float volumeGain = isMuted ? 0.0f : std::pow(10.0f, volume_.load() / 20.0f);
// Peak VU tracking
float leftPeak = -96.0f;
float rightPeak = -96.0f;
if (std::abs(volumeGain) < 0.0001f) {
// Effectively mute
for (int ch = 0; ch < channelCount_; ++ch) {
if (ch < (int)buffers_.size()) {
std::fill(buffers_[ch].begin(), buffers_[ch].begin() + n, 0.0f);
}
}
} else if (std::abs(volumeGain - 1.0f) < 0.001f) {
// Unity gain — no scaling needed, just compute VU
for (uint32_t i = 0; i < n; ++i) {
if (buffers_.size() > 0) {
float absVal = std::abs(buffers_[0][i]);
if (absVal > leftPeak) leftPeak = absVal;
}
if (buffers_.size() > 1) {
float absVal = std::abs(buffers_[1][i]);
if (absVal > rightPeak) rightPeak = absVal;
}
}
} else {
// Apply volume gain
for (int ch = 0; ch < channelCount_; ++ch) {
if (ch >= (int)buffers_.size()) break;
float* buf = buffers_[ch].data();
for (uint32_t i = 0; i < n; ++i) {
buf[i] *= volumeGain;
}
}
// Compute VU from scaled signal
for (uint32_t i = 0; i < n; ++i) {
if (buffers_.size() > 0) {
float absVal = std::abs(buffers_[0][i]);
if (absVal > leftPeak) leftPeak = absVal;
}
if (buffers_.size() > 1) {
float absVal = std::abs(buffers_[1][i]);
if (absVal > rightPeak) rightPeak = absVal;
}
}
}
// Convert peak to dB with decay
float leftDb = (leftPeak > 0.00001f) ? 20.0f * std::log10(leftPeak) : -96.0f;
float rightDb = (rightPeak > 0.00001f) ? 20.0f * std::log10(rightPeak) : -96.0f;
float oldLeft = vuLeft_.load();
float oldRight = vuRight_.load();
if (leftDb > oldLeft) {
vuLeft_ = leftDb;
} else {
vuLeft_ = oldLeft * 0.95f + leftDb * 0.05f;
}
if (rightDb > oldRight) {
vuRight_ = rightDb;
} else {
vuRight_ = oldRight * 0.95f + rightDb * 0.05f;
}
}
+245
View File
@@ -0,0 +1,245 @@
// Copyright (c) 2026 Ourpad Network
// See LICENSE file in the project root for full license text.
#include "mixer/MixerChannelStrip.hpp"
#include <algorithm>
#include <cmath>
#include <cstring>
using namespace mixer;
std::atomic<int64_t> MixerChannelStrip::nextInstanceId_{1};
MixerChannelStrip::MixerChannelStrip(int channelIndex)
: channelIndex_(channelIndex)
, instanceId_(nextInstanceId_++)
{
}
MixerChannelStrip::~MixerChannelStrip()
{
Unprepare();
}
void MixerChannelStrip::setVolume(float db)
{
volume_ = std::clamp(db, -96.0f, 12.0f);
}
void MixerChannelStrip::setPan(float pan)
{
pan_ = std::clamp(pan, -1.0f, 1.0f);
}
void MixerChannelStrip::setMute(bool mute)
{
mute_ = mute;
}
void MixerChannelStrip::setSolo(bool solo)
{
solo_ = solo;
}
void MixerChannelStrip::setAuxSend(int index, const AuxSendConfig& config)
{
if (index >= 0 && index < (int)auxSends_.size()) {
auxSends_[index] = config;
}
}
const AuxSendConfig& MixerChannelStrip::auxSend(int index) const
{
static const AuxSendConfig kDefault;
if (index >= 0 && index < (int)auxSends_.size()) {
return auxSends_[index];
}
return kDefault;
}
void MixerChannelStrip::resizeAuxSends(size_t count)
{
auxSends_.resize(count);
}
void MixerChannelStrip::setSampleRate(uint32_t sampleRate)
{
sampleRate_ = sampleRate;
hpfStates_.resize(2); // stereo HPF states
}
void MixerChannelStrip::setMaxBufferSize(size_t frames)
{
maxBufferSize_ = frames;
}
void MixerChannelStrip::allocateBuffers()
{
preFxBuffers_.clear();
postFxBuffers_.clear();
for (int i = 0; i < 2; ++i) {
preFxBuffers_.emplace_back(maxBufferSize_, 0.0f);
postFxBuffers_.emplace_back(maxBufferSize_, 0.0f);
}
}
float MixerChannelStrip::effectiveAuxLevel(int auxIndex, bool anySoloActive) const
{
if (auxIndex < 0 || auxIndex >= (int)auxSends_.size()) return -96.0f;
const auto& send = auxSends_[auxIndex];
if (!send.isActive()) return -96.0f;
// Solo overrides: if any solo is active, only soloed channels are audible
if (anySoloActive && !solo_) return -96.0f;
if (mute_) return -96.0f;
return send.level;
}
void MixerChannelStrip::applyPan(float& leftGain, float& rightGain) const
{
float pan = pan_;
// Constant-power pan law: -3dB at center
float angle = (pan * 0.5f + 0.5f) * (M_PI * 0.5f); // map -1..1 to 0..PI/2
leftGain = std::cos(angle);
rightGain = std::sin(angle);
}
void MixerChannelStrip::applyHpf(float* buffer, uint32_t frames, HpfState& state)
{
if (!hpEnabled_) return;
// Simple 1st-order IIR HPF
float fc = hpFrequency_ / sampleRate_;
float alpha = fc / (fc + 0.5f);
for (uint32_t i = 0; i < frames; ++i) {
float x = buffer[i];
float y = alpha * (state.y1 + x - state.x1);
state.x1 = x;
state.y1 = y;
buffer[i] = y;
}
}
void MixerChannelStrip::process(
const float* const* inputBuffers,
size_t inputChannels,
float* const* outputBuffers,
size_t outputChannels,
uint32_t frames)
{
// Clamp frames to allocated buffer size
frames = std::min(frames, (uint32_t)maxBufferSize_);
// Step 1: Copy input to pre-FX buffers and apply HPF
for (size_t ch = 0; ch < std::min(inputChannels, (size_t)2); ++ch) {
if (ch < preFxBuffers_.size() && inputBuffers[ch]) {
std::copy(inputBuffers[ch], inputBuffers[ch] + frames,
preFxBuffers_[ch].begin());
applyHpf(preFxBuffers_[ch].data(), frames,
ch < hpfStates_.size() ? hpfStates_[ch] : hpfStates_[0]);
}
}
// Step 2: For standalone v1, there is no FX chain.
// Simply copy pre-FX to post-FX buffers (passthrough).
// Handle mono-to-stereo expansion: mono input feeds both channels.
if (inputChannels == 1 && postFxBuffers_.size() >= 2 && preFxBuffers_.size() >= 1) {
// Mono → stereo: copy same signal to both post-FX channels
std::copy(preFxBuffers_[0].begin(),
preFxBuffers_[0].begin() + frames,
postFxBuffers_[0].begin());
std::copy(preFxBuffers_[0].begin(),
preFxBuffers_[0].begin() + frames,
postFxBuffers_[1].begin());
} else {
for (size_t ch = 0; ch < std::min(inputChannels, (size_t)2); ++ch) {
if (ch < postFxBuffers_.size() && ch < preFxBuffers_.size()) {
std::copy(preFxBuffers_[ch].begin(),
preFxBuffers_[ch].begin() + frames,
postFxBuffers_[ch].begin());
}
}
}
// Step 3: Apply volume, pan, and mute/solo to create output
bool isMuted = mute_.load();
bool isSoloed = solo_.load();
// Calculate gain from volume dB
float volumeGain = isMuted ? 0.0f : std::pow(10.0f, volume_.load() / 20.0f);
// Calculate pan gains
float leftGain = 1.0f, rightGain = 1.0f;
applyPan(leftGain, rightGain);
// Apply to output buffers
for (size_t outCh = 0; outCh < std::min(outputChannels, (size_t)2); ++outCh) {
if (!outputBuffers[outCh]) continue;
float* dst = outputBuffers[outCh];
const float* src = (outCh < postFxBuffers_.size())
? postFxBuffers_[outCh].data()
: (postFxBuffers_.empty() ? nullptr : postFxBuffers_[0].data());
if (!src) {
std::fill(dst, dst + frames, 0.0f);
continue;
}
float panGain = (outCh == 0) ? leftGain : rightGain;
float finalGain = volumeGain * panGain;
if (finalGain < 0.001f) {
std::fill(dst, dst + frames, 0.0f);
} else if (std::abs(finalGain - 1.0f) < 0.001f) {
std::copy(src, src + frames, dst);
} else {
for (uint32_t i = 0; i < frames; ++i) {
dst[i] = src[i] * finalGain;
}
}
}
// Step 4: Update VU meters (peak, with 300ms decay)
for (size_t ch = 0; ch < std::min(outputChannels, (size_t)2); ++ch) {
if (ch >= postFxBuffers_.size()) break;
float peak = 0.0f;
const float* buf = postFxBuffers_[ch].data();
for (uint32_t i = 0; i < frames; ++i) {
float absVal = std::abs(buf[i]);
if (absVal > peak) peak = absVal;
}
float peakDb = (peak > 0.00001f) ? 20.0f * std::log10(peak) : -96.0f;
// Decay: 300ms time constant
float& vu = (ch == 0) ? vuLeft_ : vuRight_;
if (peakDb > vu) {
vu = peakDb; // Instant attack
} else {
// Decay at ~300ms
static const float releaseRate = 0.95f;
vu = vu * releaseRate + peakDb * (1.0f - releaseRate);
}
}
}
void MixerChannelStrip::Activate()
{
// No FX chain to activate in standalone v1
}
void MixerChannelStrip::Deactivate()
{
// No FX chain to deactivate in standalone v1
}
void MixerChannelStrip::Unprepare()
{
preFxBuffers_.clear();
postFxBuffers_.clear();
}
+372
View File
@@ -0,0 +1,372 @@
// Copyright (c) 2026 Ourpad Network
// See LICENSE file in the project root for full license text.
#include "mixer/MixerControlServer.hpp"
#include "mixer/MixerEngine.hpp"
#include "mixer/MixerChannelStrip.hpp"
#include "mixer/MixerBus.hpp"
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/stat.h>
#include <unistd.h>
#include <cstring>
#include <cstdio>
#include <sstream>
#include <algorithm>
#include <nlohmann/json.hpp>
using namespace mixer;
using json = nlohmann::json;
MixerControlServer::MixerControlServer(MixerEngine* engine, const std::string& socketPath)
: engine_(engine)
, socketPath_(socketPath)
{
}
MixerControlServer::~MixerControlServer()
{
stop();
}
bool MixerControlServer::start()
{
// Remove existing socket file if present
unlink(socketPath_.c_str());
serverFd_ = socket(AF_UNIX, SOCK_STREAM, 0);
if (serverFd_ < 0) {
perror("socket");
return false;
}
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_);
return false;
}
// Set directory permissions so any local process can connect
chmod(socketPath_.c_str(), 0666);
if (listen(serverFd_, 5) < 0) {
perror("listen");
close(serverFd_);
return false;
}
running_ = true;
serverThread_ = std::thread(&MixerControlServer::serverLoop, this);
return true;
}
void MixerControlServer::stop()
{
running_ = false;
if (serverFd_ >= 0) {
close(serverFd_);
serverFd_ = -1;
}
if (serverThread_.joinable()) {
serverThread_.join();
}
unlink(socketPath_.c_str());
}
void MixerControlServer::serverLoop()
{
while (running_) {
struct sockaddr_un clientAddr;
socklen_t clientLen = sizeof(clientAddr);
int clientFd = accept(serverFd_, (struct sockaddr*)&clientAddr, &clientLen);
if (clientFd < 0) {
if (running_) {
perror("accept");
}
continue;
}
handleClient(clientFd);
close(clientFd);
}
}
void MixerControlServer::handleClient(int clientFd)
{
char buffer[65536];
ssize_t n = read(clientFd, buffer, sizeof(buffer) - 1);
if (n <= 0) return;
buffer[n] = '\0';
// Process each line as a separate command
std::string data(buffer);
std::istringstream stream(data);
std::string line;
while (std::getline(stream, line)) {
// Trim whitespace
line.erase(line.begin(), std::find_if(line.begin(), line.end(),
[](unsigned char c) { return !std::isspace(c); }));
line.erase(std::find_if(line.rbegin(), line.rend(),
[](unsigned char c) { return !std::isspace(c); }).base(), line.end());
if (line.empty()) continue;
std::string response = processCommand(line);
response += '\n';
write(clientFd, response.c_str(), response.size());
}
}
std::string MixerControlServer::processCommand(const std::string& commandJson)
{
try {
json cmd = json::parse(commandJson);
if (!cmd.contains("cmd")) {
return json({{"status", "error"}, {"message", "missing 'cmd' field"}}).dump();
}
std::string command = cmd["cmd"];
if (command == "getState") {
return buildStateJson();
}
if (command == "getVU") {
return buildVuJson();
}
if (command == "setChannelVolume") {
int channel = cmd.value("channel", 0);
float volume = cmd.value("volume", 0.0f);
engine_->getChannel(channel)->setVolume(volume);
return json({{"status", "ok"}}).dump();
}
if (command == "setChannelPan") {
int channel = cmd.value("channel", 0);
float pan = cmd.value("pan", 0.0f);
engine_->getChannel(channel)->setPan(pan);
return json({{"status", "ok"}}).dump();
}
if (command == "setChannelMute") {
int channel = cmd.value("channel", 0);
bool mute = cmd.value("mute", false);
engine_->getChannel(channel)->setMute(mute);
return json({{"status", "ok"}}).dump();
}
if (command == "setChannelSolo") {
int channel = cmd.value("channel", 0);
bool solo = cmd.value("solo", false);
engine_->getChannel(channel)->setSolo(solo);
return json({{"status", "ok"}}).dump();
}
if (command == "setChannelLabel") {
int channel = cmd.value("channel", 0);
std::string label = cmd.value("label", "");
engine_->getChannel(channel)->setLabel(label);
return json({{"status", "ok"}}).dump();
}
if (command == "setBusVolume") {
int64_t busId = cmd.value("busId", (int64_t)0);
float volume = cmd.value("volume", 0.0f);
auto* bus = engine_->getBus(busId);
if (bus) bus->setVolume(volume);
return json({{"status", "ok"}}).dump();
}
if (command == "setBusMute") {
int64_t busId = cmd.value("busId", (int64_t)0);
bool mute = cmd.value("mute", false);
auto* bus = engine_->getBus(busId);
if (bus) bus->setMute(mute);
return json({{"status", "ok"}}).dump();
}
if (command == "routeChannelToBus") {
int channel = cmd.value("channel", 0);
int64_t busId = cmd.value("busId", (int64_t)0);
float level = cmd.value("level", 0.0f);
engine_->routeChannelToBus(channel, busId, level);
return json({{"status", "ok"}}).dump();
}
if (command == "routeBusToBus") {
int64_t sourceBusId = cmd.value("sourceBusId", (int64_t)0);
int64_t targetBusId = cmd.value("targetBusId", (int64_t)0);
float level = cmd.value("level", 0.0f);
engine_->routeBusToBus(sourceBusId, targetBusId, level);
return json({{"status", "ok"}}).dump();
}
if (command == "removeRoute") {
int64_t sourceId = cmd.value("sourceId", (int64_t)0);
int64_t targetBusId = cmd.value("targetBusId", (int64_t)0);
engine_->removeRoute(sourceId, targetBusId);
return json({{"status", "ok"}}).dump();
}
if (command == "autoCreateChannels") {
uint32_t inputs = cmd.value("inputs", (uint32_t)2);
uint32_t outputs = cmd.value("outputs", (uint32_t)2);
engine_->autoCreateChannels(inputs, outputs);
return json({{"status", "ok"}}).dump();
}
if (command == "addChannel") {
int physicalInput = cmd.value("physicalInput", 0);
auto* channel = engine_->addChannel(physicalInput);
int idx = channel ? channel->channelIndex() : -1;
return json({{"status", "ok"}, {"channelIndex", idx}}).dump();
}
if (command == "removeChannel") {
int channel = cmd.value("channel", 0);
engine_->removeChannel(channel);
return json({{"status", "ok"}}).dump();
}
return json({{"status", "error"}, {"message", "unknown command: " + command}}).dump();
} catch (const json::parse_error& e) {
return json({{"status", "error"}, {"message", std::string("JSON parse error: ") + e.what()}}).dump();
} catch (const std::exception& e) {
return json({{"status", "error"}, {"message", std::string("error: ") + e.what()}}).dump();
}
}
std::string MixerControlServer::buildStateJson() const
{
auto snapshot = engine_->captureSnapshot();
json state;
state["sampleRate"] = 48000; // TODO: expose from engine
// Encode channels
json channels = json::array();
for (const auto& cs : snapshot.channels) {
json ch;
ch["channelIndex"] = cs.channelIndex;
ch["volume"] = cs.volume;
ch["pan"] = cs.pan;
ch["mute"] = cs.mute;
ch["solo"] = cs.solo;
ch["hpEnabled"] = cs.hpEnabled;
ch["hpFrequency"] = cs.hpFrequency;
ch["label"] = cs.label;
const char* typeStr = "Instrument";
switch (cs.channelType) {
case MixerChannelType::Mic: typeStr = "Mic"; break;
case MixerChannelType::Line: typeStr = "Line"; break;
case MixerChannelType::AuxReturn: typeStr = "AuxReturn"; break;
default: typeStr = "Instrument"; break;
}
ch["type"] = typeStr;
json auxLevels = json::array();
for (float level : cs.auxSendLevels) {
auxLevels.push_back(level);
}
ch["auxSendLevels"] = auxLevels;
channels.push_back(ch);
}
state["channels"] = channels;
// Encode buses
json buses = json::array();
for (const auto& bs : snapshot.buses) {
json bus;
bus["id"] = bs.id;
bus["name"] = bs.name;
bus["volume"] = bs.volume;
bus["mute"] = bs.mute;
const char* typeStr = "Subgroup";
switch (bs.type) {
case MixerBusType::Master: typeStr = "Master"; break;
case MixerBusType::Aux: typeStr = "Aux"; break;
case MixerBusType::FxReturn: typeStr = "FxReturn"; break;
default: typeStr = "Subgroup"; break;
}
bus["type"] = typeStr;
buses.push_back(bus);
}
state["buses"] = buses;
// Encode routes
json routes = json::array();
for (const auto& route : snapshot.routes) {
json r;
r["sourceId"] = route.sourceId;
r["targetBusId"] = route.targetBusId;
r["level"] = route.level;
r["sourceType"] = (route.sourceType == MixerRouteEntry::SourceChannel) ? "channel" : "bus";
routes.push_back(r);
}
state["routes"] = routes;
// Output routes
json outRoutes = json::array();
for (const auto& route : engine_->outputRoutes()) {
json r;
r["sourceBusId"] = route.sourceBusId;
r["sourceStartChannel"] = route.sourceStartChannel;
r["targetStartChannel"] = route.targetStartChannel;
r["channels"] = route.channels;
outRoutes.push_back(r);
}
state["outputRoutes"] = outRoutes;
// Physical I/O
state["physicalInputCount"] = engine_->physicalInputCount();
state["physicalOutputCount"] = engine_->physicalOutputCount();
return state.dump();
}
std::string MixerControlServer::buildVuJson() const
{
json vuReport;
json channels = json::array();
for (size_t i = 0; i < engine_->channelCount(); ++i) {
auto* ch = engine_->getChannel(i);
if (!ch) continue;
json chVu;
chVu["channelIndex"] = i;
chVu["left"] = ch->vuLeft();
chVu["right"] = ch->vuRight();
channels.push_back(chVu);
}
vuReport["channels"] = channels;
json buses = json::array();
for (auto busId : engine_->busIds()) {
auto* bus = engine_->getBus(busId);
if (!bus) continue;
json busVu;
busVu["busId"] = busId;
busVu["left"] = bus->vuLeft();
busVu["right"] = bus->vuRight();
buses.push_back(busVu);
}
vuReport["buses"] = buses;
return vuReport.dump();
}
+615
View File
@@ -0,0 +1,615 @@
// Copyright (c) 2026 Ourpad Network
// See LICENSE file in the project root for full license text.
#include "mixer/MixerEngine.hpp"
#include "mixer/MixerChannelStrip.hpp"
#include "mixer/MixerBus.hpp"
#include <algorithm>
#include <cmath>
using namespace mixer;
std::atomic<int64_t> MixerEngine::nextBusId_{1};
MixerEngine::MixerEngine()
{
// Create the master bus by default
int64_t masterId = nextBusId_++;
auto master = std::make_unique<MixerBus>(masterId, MixerBusType::Master, "Master", 2);
masterBus_ = master.get();
buses_[masterId] = std::move(master);
}
MixerEngine::~MixerEngine()
{
Deactivate();
}
void MixerEngine::setSampleRate(uint32_t sampleRate)
{
sampleRate_ = sampleRate;
}
void MixerEngine::setMaxBufferSize(size_t frames)
{
maxBufferSize_ = frames;
}
// --- Channel Management ---
MixerChannelStrip* MixerEngine::addChannel(int physicalInputIndex)
{
auto channel = std::make_unique<MixerChannelStrip>(physicalInputIndex);
channel->setSampleRate(sampleRate_);
channel->setMaxBufferSize(maxBufferSize_);
channel->setLabel("Channel " + std::to_string(physicalInputIndex + 1));
auto* ptr = channel.get();
channels_.push_back(std::move(channel));
// Create default route: this channel → master bus at unity
MixerRouteEntry route;
route.sourceType = MixerRouteEntry::SourceChannel;
route.sourceId = ptr->instanceId();
route.targetBusId = masterBus_->id();
route.level = 0.0f; // unity
routes_.push_back(route);
return ptr;
}
void MixerEngine::removeChannel(int channelIndex)
{
if (channelIndex < 0 || channelIndex >= (int)channels_.size()) return;
auto* channel = channels_[channelIndex].get();
int64_t instanceId = channel->instanceId();
// Remove all routes referencing this channel
routes_.erase(
std::remove_if(routes_.begin(), routes_.end(),
[instanceId](const MixerRouteEntry& r) {
return r.sourceType == MixerRouteEntry::SourceChannel &&
r.sourceId == instanceId;
}),
routes_.end()
);
channel->Unprepare();
channels_.erase(channels_.begin() + channelIndex);
}
MixerChannelStrip* MixerEngine::getChannel(int channelIndex)
{
if (channelIndex >= 0 && channelIndex < (int)channels_.size())
return channels_[channelIndex].get();
return nullptr;
}
const MixerChannelStrip* MixerEngine::getChannel(int channelIndex) const
{
if (channelIndex >= 0 && channelIndex < (int)channels_.size())
return channels_[channelIndex].get();
return nullptr;
}
// --- Bus Management ---
int64_t MixerEngine::addBus(MixerBusType type, const std::string& name, int channels)
{
int64_t id = nextBusId_++;
auto bus = std::make_unique<MixerBus>(id, type, name, channels);
bus->allocateBuffers(maxBufferSize_);
buses_[id] = std::move(bus);
return id;
}
void MixerEngine::removeBus(int64_t busId)
{
if (busId == masterBus_->id()) return; // Can't remove master
// Remove all routes targeting this bus
routes_.erase(
std::remove_if(routes_.begin(), routes_.end(),
[busId](const MixerRouteEntry& r) {
return r.targetBusId == busId;
}),
routes_.end()
);
buses_.erase(busId);
}
MixerBus* MixerEngine::getBus(int64_t busId)
{
auto it = buses_.find(busId);
return (it != buses_.end()) ? it->second.get() : nullptr;
}
const MixerBus* MixerEngine::getBus(int64_t busId) const
{
auto it = buses_.find(busId);
return (it != buses_.end()) ? it->second.get() : nullptr;
}
std::vector<int64_t> MixerEngine::busIds() const
{
std::vector<int64_t> ids;
ids.reserve(buses_.size());
for (const auto& [id, _] : buses_) {
ids.push_back(id);
}
return ids;
}
// --- Routing ---
void MixerEngine::routeChannelToBus(int channelIndex, int64_t busId, float levelDb)
{
if (channelIndex < 0 || channelIndex >= (int)channels_.size()) return;
if (!getBus(busId)) return;
auto* channel = channels_[channelIndex].get();
// Check if route already exists — update level
for (auto& route : routes_) {
if (route.sourceType == MixerRouteEntry::SourceChannel &&
route.sourceId == channel->instanceId() &&
route.targetBusId == busId) {
route.level = levelDb;
return;
}
}
// Add new route
MixerRouteEntry route;
route.sourceType = MixerRouteEntry::SourceChannel;
route.sourceId = channel->instanceId();
route.targetBusId = busId;
route.level = levelDb;
routes_.push_back(route);
}
void MixerEngine::routeBusToBus(int64_t sourceBusId, int64_t targetBusId, float levelDb)
{
if (!getBus(sourceBusId) || !getBus(targetBusId)) return;
for (auto& route : routes_) {
if (route.sourceType == MixerRouteEntry::SourceBus &&
route.sourceId == sourceBusId &&
route.targetBusId == targetBusId) {
route.level = levelDb;
return;
}
}
MixerRouteEntry route;
route.sourceType = MixerRouteEntry::SourceBus;
route.sourceId = sourceBusId;
route.targetBusId = targetBusId;
route.level = levelDb;
routes_.push_back(route);
}
void MixerEngine::removeRoute(int64_t sourceId, int64_t targetBusId)
{
routes_.erase(
std::remove_if(routes_.begin(), routes_.end(),
[sourceId, targetBusId](const MixerRouteEntry& r) {
return r.sourceId == sourceId && r.targetBusId == targetBusId;
}),
routes_.end()
);
}
void MixerEngine::clearRoutes()
{
routes_.clear();
}
// --- Lifecycle ---
void MixerEngine::Prepare()
{
// Allocate bus buffers
for (auto& [_, bus] : buses_) {
bus->allocateBuffers(maxBufferSize_);
}
// Allocate per-channel output buffers (for routing accumulation)
channelOutputBuffers_.resize(std::max((size_t)1, channels_.size()));
for (auto& buf : channelOutputBuffers_) {
buf.resize(maxBufferSize_ * 2, 0.0f); // stereo output per channel
}
// Configure each channel
for (auto& channel : channels_) {
channel->setSampleRate(sampleRate_);
channel->setMaxBufferSize(maxBufferSize_);
channel->allocateBuffers();
}
}
void MixerEngine::Activate()
{
for (auto& channel : channels_) {
channel->Activate();
}
}
void MixerEngine::Deactivate()
{
for (auto& channel : channels_) {
channel->Deactivate();
}
}
// --- Solo ---
bool MixerEngine::anySoloActive() const
{
for (const auto& channel : channels_) {
if (channel->solo()) return true;
}
return false;
}
// --- Audio Processing ---
std::vector<MixerRouteEntry*> MixerEngine::findRoutesForSource(int64_t sourceId)
{
std::vector<MixerRouteEntry*> result;
for (auto& route : routes_) {
if (route.sourceId == sourceId) {
result.push_back(&route);
}
}
return result;
}
void MixerEngine::routeChannelOutput(
MixerChannelStrip* channel,
float** channelOutput,
uint32_t frames)
{
bool soloActive = anySoloActive();
// Find all routes for this channel
int64_t channelId = channel->instanceId();
auto channelRoutes = findRoutesForSource(channelId);
for (auto* route : channelRoutes) {
MixerBus* targetBus = getBus(route->targetBusId);
if (!targetBus) continue;
float levelLinear = std::pow(10.0f, route->level / 20.0f);
targetBus->accumulate(
(const float* const*)channelOutput,
frames,
levelLinear,
2 // channelOutput is always stereo
);
}
// Process aux sends
size_t numAuxSends = channel->auxSendCount();
for (size_t auxIdx = 0; auxIdx < numAuxSends; ++auxIdx) {
float effectiveLevel = channel->effectiveAuxLevel(auxIdx, soloActive);
if (effectiveLevel < -90.0f) continue;
const auto& sendConfig = channel->auxSend(auxIdx);
MixerBus* auxBus = getBus(sendConfig.targetBusId);
if (!auxBus) continue;
float sendGain = std::pow(10.0f, effectiveLevel / 20.0f);
if (sendConfig.preFader) {
// Pre-fader: use the pre-FX buffer
const float* preFx0 = channel->preFxBuffer(0);
const float* preFx1 = channel->preFxBuffer(1);
if (preFx0) {
const float* preFx[2] = { preFx0, preFx1 };
auxBus->accumulate(preFx, frames, sendGain, 2);
}
} else {
// Post-fader: use the same output that goes to buses
auxBus->accumulate(
(const float* const*)channelOutput,
frames,
sendGain,
2
);
}
}
}
void MixerEngine::processBusRouting(uint32_t frames)
{
for (auto& route : routes_) {
if (route.sourceType != MixerRouteEntry::SourceBus) continue;
MixerBus* sourceBus = getBus(route.sourceId);
MixerBus* targetBus = getBus(route.targetBusId);
if (!sourceBus || !targetBus) continue;
int nChannels = sourceBus->channelCount();
std::vector<const float*> srcPtrs(nChannels);
for (int ch = 0; ch < nChannels; ++ch) {
srcPtrs[ch] = sourceBus->buffer(ch);
}
float levelLinear = std::pow(10.0f, route.level / 20.0f);
targetBus->accumulate(srcPtrs.data(), frames, levelLinear, nChannels);
}
}
void MixerEngine::process(
float** deviceInputs,
uint32_t inputChannels,
float** deviceOutputs,
uint32_t outputChannels,
uint32_t frames)
{
// Clamp
frames = std::min(frames, (uint32_t)maxBufferSize_);
// Step 1: Clear all bus buffers
for (auto& [_, bus] : buses_) {
bus->clear();
}
// Step 2: Process each channel
size_t numChannels = channels_.size();
// Determine channel pairing mode
bool stereoPairing = (inputChannels <= 2);
for (size_t ch = 0; ch < numChannels; ++ch) {
auto* channel = channels_[ch].get();
// Build input buffer pointers for this channel
float* channelInputs[2] = { nullptr, nullptr };
if (stereoPairing) {
uint32_t baseInput = (uint32_t)(ch * 2);
if (baseInput < inputChannels) {
channelInputs[0] = deviceInputs[baseInput];
if (baseInput + 1 < inputChannels) {
channelInputs[1] = deviceInputs[baseInput + 1];
}
}
} else {
if (ch < inputChannels) {
channelInputs[0] = deviceInputs[ch];
}
}
// Build output buffer (stereo, from our per-channel scratch buffers)
float* channelOutputs[2] = { nullptr, nullptr };
if (ch < channelOutputBuffers_.size()) {
channelOutputs[0] = channelOutputBuffers_[ch].data();
channelOutputs[1] = channelOutputBuffers_[ch].data() + maxBufferSize_;
}
// Determine the actual number of input channels for this strip
size_t stripInputChannels = 0;
if (channelInputs[0] != nullptr) stripInputChannels = 1;
if (channelInputs[1] != nullptr) stripInputChannels = 2;
// Process the channel strip
channel->process(
(const float* const*)channelInputs,
stripInputChannels,
channelOutputs,
2,
frames
);
// Route channel output to buses
routeChannelOutput(channel, channelOutputs, frames);
}
// Step 3: Process bus-to-bus routing
processBusRouting(frames);
// Step 4: Process each bus (apply volume, compute VU)
for (auto& [_, bus] : buses_) {
bus->process(frames);
}
// Step 5: Write buses to physical outputs according to output routing
if (outputRoutes_.empty()) {
// Legacy fallback: write master bus to device outputs 1:1
if (masterBus_) {
for (uint32_t outCh = 0; outCh < outputChannels; ++outCh) {
if (deviceOutputs[outCh] == nullptr) continue;
const float* src = masterBus_->buffer(outCh);
if (src) {
std::copy(src, src + frames, deviceOutputs[outCh]);
} else if (outCh == 1) {
const float* srcL = masterBus_->buffer(0);
if (srcL) {
std::copy(srcL, srcL + frames, deviceOutputs[outCh]);
}
}
}
}
} else {
// Use configured output routes
for (const auto& route : outputRoutes_) {
MixerBus* sourceBus = getBus(route.sourceBusId);
if (!sourceBus) continue;
for (int ch = 0; ch < route.channels; ++ch) {
uint32_t targetCh = (uint32_t)(route.targetStartChannel + ch);
if (targetCh >= outputChannels) break;
if (deviceOutputs[targetCh] == nullptr) continue;
const float* src = sourceBus->buffer(route.sourceStartChannel + ch);
if (src) {
std::copy(src, src + frames, deviceOutputs[targetCh]);
}
}
}
}
}
// --- State Serialization ---
MixerSnapshot MixerEngine::captureSnapshot() const
{
MixerSnapshot snap;
for (const auto& channel : channels_) {
MixerSnapshot::ChannelState cs;
cs.channelIndex = channel->channelIndex();
cs.volume = channel->volume();
cs.pan = channel->pan();
cs.mute = channel->mute();
cs.solo = channel->solo();
cs.channelType = channel->channelType();
cs.label = channel->label();
cs.hpEnabled = channel->hpEnabled();
cs.hpFrequency = channel->hpFrequency();
for (size_t i = 0; i < channel->auxSendCount(); ++i) {
cs.auxSendLevels.push_back(channel->auxSend(i).level);
}
snap.channels.push_back(cs);
}
for (const auto& [id, bus] : buses_) {
MixerSnapshot::BusState bs;
bs.id = id;
bs.name = bus->name();
bs.type = bus->type();
bs.volume = bus->volume();
bs.mute = bus->mute();
snap.buses.push_back(bs);
}
snap.routes = routes_;
return snap;
}
// --- Output Routing ---
void MixerEngine::setOutputRoutes(const std::vector<MixerOutputRoute>& routes)
{
outputRoutes_ = routes;
}
void MixerEngine::addOutputRoute(int64_t busId, int sourceStartChannel, int targetStartChannel, int channels)
{
// Remove any existing route that conflicts with the target
outputRoutes_.erase(
std::remove_if(outputRoutes_.begin(), outputRoutes_.end(),
[busId, targetStartChannel](const MixerOutputRoute& r) {
return r.sourceBusId == busId &&
r.targetStartChannel == targetStartChannel;
}),
outputRoutes_.end()
);
MixerOutputRoute route;
route.sourceBusId = busId;
route.sourceStartChannel = sourceStartChannel;
route.targetStartChannel = targetStartChannel;
route.channels = channels;
outputRoutes_.push_back(route);
}
void MixerEngine::removeOutputRoutes(int64_t busId)
{
outputRoutes_.erase(
std::remove_if(outputRoutes_.begin(), outputRoutes_.end(),
[busId](const MixerOutputRoute& r) { return r.sourceBusId == busId; }),
outputRoutes_.end()
);
}
std::vector<MixerOutputRoute> MixerEngine::findOutputRoutesForBus(int64_t busId) const
{
std::vector<MixerOutputRoute> result;
for (const auto& route : outputRoutes_) {
if (route.sourceBusId == busId) {
result.push_back(route);
}
}
return result;
}
// --- Auto channel creation ---
void MixerEngine::autoCreateChannels(uint32_t inputChannelCount, uint32_t outputChannelCount)
{
physicalInputCount_ = inputChannelCount;
physicalOutputCount_ = outputChannelCount;
// Remove existing channels if any
for (int i = (int)channels_.size() - 1; i >= 0; --i) {
removeChannel(i);
}
// Create one channel strip per input
for (uint32_t i = 0; i < inputChannelCount; ++i) {
addChannel((int)i);
auto* ch = getChannel((int)i);
if (ch) {
char label[32];
snprintf(label, sizeof(label), "Input %u", i + 1);
ch->setLabel(label);
}
}
// Set default output routes
outputRoutes_.clear();
if (!masterBus_) return;
MixerOutputRoute masterRoute;
masterRoute.sourceBusId = masterBus_->id();
masterRoute.sourceStartChannel = 0;
masterRoute.targetStartChannel = 0;
masterRoute.channels = 2;
outputRoutes_.push_back(masterRoute);
}
void MixerEngine::applySnapshot(const MixerSnapshot& snapshot)
{
// Apply channel states
for (const auto& cs : snapshot.channels) {
auto* channel = getChannel(cs.channelIndex);
if (!channel) continue;
channel->setVolume(cs.volume);
channel->setPan(cs.pan);
channel->setMute(cs.mute);
channel->setSolo(cs.solo);
channel->setChannelType(cs.channelType);
channel->setLabel(cs.label);
channel->setHpEnabled(cs.hpEnabled);
channel->setHpFrequency(cs.hpFrequency);
for (size_t i = 0; i < cs.auxSendLevels.size() && i < channel->auxSendCount(); ++i) {
auto config = channel->auxSend(i);
config.level = cs.auxSendLevels[i];
channel->setAuxSend(i, config);
}
}
// Apply bus states
for (const auto& bs : snapshot.buses) {
auto* bus = getBus(bs.id);
if (!bus) continue;
bus->setName(bs.name);
bus->setVolume(bs.volume);
bus->setMute(bs.mute);
}
// Replace routes
routes_ = snapshot.routes;
}
+238
View File
@@ -0,0 +1,238 @@
// Copyright (c) 2026 Ourpad Network
// See LICENSE file in the project root for full license text.
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <signal.h>
#include <atomic>
#include <string>
#include <vector>
#include <jack/jack.h>
#include "mixer/MixerEngine.hpp"
#include "mixer/MixerControlServer.hpp"
// ---------------------------------------------------------------------------
// Globals (for JACK callback — no C++ context pointer in plain jack_process)
// ---------------------------------------------------------------------------
static mixer::MixerEngine* g_engine = nullptr;
static jack_client_t* g_jackClient = nullptr;
static std::vector<jack_port_t*> g_inputPorts;
static std::vector<jack_port_t*> g_outputPorts;
static std::atomic<bool> g_running{true};
static const char* kJackClientName = "mixer-daemon";
// ---------------------------------------------------------------------------
// Forward signal handler
// ---------------------------------------------------------------------------
static void signalHandler(int sig)
{
(void)sig;
g_running = false;
}
// ---------------------------------------------------------------------------
// JACK process callback (real-time audio thread)
// ---------------------------------------------------------------------------
static int jackProcessCallback(jack_nframes_t nframes, void* /*arg*/)
{
if (!g_engine) return 0;
uint32_t inputChannels = (uint32_t)g_inputPorts.size();
uint32_t outputChannels = (uint32_t)g_outputPorts.size();
// Build device input buffer array from JACK ports
std::vector<float*> deviceInputs(inputChannels, nullptr);
for (uint32_t i = 0; i < inputChannels; ++i) {
deviceInputs[i] = (float*)jack_port_get_buffer(g_inputPorts[i], nframes);
}
// Build device output buffer array from JACK ports
std::vector<float*> deviceOutputs(outputChannels, nullptr);
for (uint32_t i = 0; i < outputChannels; ++i) {
deviceOutputs[i] = (float*)jack_port_get_buffer(g_outputPorts[i], nframes);
// Zero output buffers
memset(deviceOutputs[i], 0, nframes * sizeof(float));
}
// Run mixer engine (reads inputs, processes, writes to outputs)
g_engine->process(
deviceInputs.data(),
inputChannels,
deviceOutputs.data(),
outputChannels,
nframes
);
return 0;
}
// ---------------------------------------------------------------------------
// JACK shutdown callback
// ---------------------------------------------------------------------------
static void jackShutdownCallback(void* /*arg*/)
{
fprintf(stderr, "JACK server shutdown.\n");
g_running = false;
}
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
int main(int argc, char* argv[])
{
const char* socketPath = "/tmp/mixer-daemon.sock";
int inputChannels = 2;
int outputChannels = 2;
uint32_t sampleRate = 48000;
uint32_t bufferSize = 256;
// Parse command line args
for (int i = 1; i < argc; ++i) {
if (strcmp(argv[i], "--socket") == 0 && i + 1 < argc) {
socketPath = argv[++i];
} else if (strcmp(argv[i], "--inputs") == 0 && i + 1 < argc) {
inputChannels = atoi(argv[++i]);
} else if (strcmp(argv[i], "--outputs") == 0 && i + 1 < argc) {
outputChannels = atoi(argv[++i]);
} else if (strcmp(argv[i], "--sample-rate") == 0 && i + 1 < argc) {
sampleRate = (uint32_t)atoi(argv[++i]);
} else if (strcmp(argv[i], "--buffer-size") == 0 && i + 1 < argc) {
bufferSize = (uint32_t)atoi(argv[++i]);
} else if (strcmp(argv[i], "--help") == 0) {
printf("Usage: mixer-daemon [options]\n");
printf(" --socket <path> Unix socket path (default: /tmp/mixer-daemon.sock)\n");
printf(" --inputs <N> Number of JACK input channels (default: 2)\n");
printf(" --outputs <N> Number of JACK output channels (default: 2)\n");
printf(" --sample-rate <N> Sample rate (default: 48000)\n");
printf(" --buffer-size <N> Buffer size in frames (default: 256)\n");
printf(" --help Show this help\n");
return 0;
}
}
fprintf(stderr, "mixer-daemon starting...\n");
fprintf(stderr, " Socket: %s\n", socketPath);
fprintf(stderr, " Channels: %d in / %d out\n", inputChannels, outputChannels);
fprintf(stderr, " Sample rate: %u, Buffer: %u\n", sampleRate, bufferSize);
// --- Setup signal handlers ---
signal(SIGINT, signalHandler);
signal(SIGTERM, signalHandler);
// --- Create MixerEngine ---
mixer::MixerEngine engine;
engine.setSampleRate(sampleRate);
engine.setMaxBufferSize(bufferSize);
engine.autoCreateChannels(inputChannels, outputChannels);
engine.Prepare();
// --- Open JACK client ---
jack_status_t jackStatus;
g_jackClient = jack_client_open(kJackClientName, JackNullOption, &jackStatus);
if (!g_jackClient) {
fprintf(stderr, "Failed to open JACK client (status=0x%x). Is JACK running?\n", jackStatus);
// Fallback: run without JACK (dry/headless mode)
fprintf(stderr, "Running in headless mode (no JACK audio I/O).\n");
g_engine = &engine;
// Still create the mixer control server
mixer::MixerControlServer server(&engine, socketPath);
if (!server.start()) {
fprintf(stderr, "Failed to start control server at %s\n", socketPath);
return 1;
}
fprintf(stderr, "Control server running at %s. Press Ctrl+C to stop.\n", socketPath);
// Wait for shutdown signal
while (g_running) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
server.stop();
return 0;
}
// Get actual sample rate and buffer size from JACK
sampleRate = jack_get_sample_rate(g_jackClient);
bufferSize = jack_get_buffer_size(g_jackClient);
fprintf(stderr, "JACK active — sample rate: %u, buffer: %u\n", sampleRate, bufferSize);
// Reconfigure engine with actual JACK parameters
engine.setSampleRate(sampleRate);
engine.setMaxBufferSize(bufferSize);
engine.autoCreateChannels(inputChannels, outputChannels);
engine.Prepare();
g_engine = &engine;
// --- Register JACK ports ---
for (int i = 0; i < inputChannels; ++i) {
char name[32];
snprintf(name, sizeof(name), "input_%d", i + 1);
jack_port_t* port = jack_port_register(
g_jackClient, name, JACK_DEFAULT_AUDIO_TYPE,
JackPortIsInput, 0);
if (!port) {
fprintf(stderr, "Failed to register input port %s\n", name);
} else {
g_inputPorts.push_back(port);
}
}
for (int i = 0; i < outputChannels; ++i) {
char name[32];
snprintf(name, sizeof(name), "output_%d", i + 1);
jack_port_t* port = jack_port_register(
g_jackClient, name, JACK_DEFAULT_AUDIO_TYPE,
JackPortIsOutput, 0);
if (!port) {
fprintf(stderr, "Failed to register output port %s\n", name);
} else {
g_outputPorts.push_back(port);
}
}
fprintf(stderr, "Registered %zu input ports and %zu output ports.\n",
g_inputPorts.size(), g_outputPorts.size());
// --- Set JACK callbacks ---
jack_set_process_callback(g_jackClient, jackProcessCallback, nullptr);
jack_on_shutdown(g_jackClient, jackShutdownCallback, nullptr);
// --- Activate JACK client ---
if (jack_activate(g_jackClient) != 0) {
fprintf(stderr, "Failed to activate JACK client.\n");
jack_client_close(g_jackClient);
return 1;
}
// --- Start control server ---
mixer::MixerControlServer server(&engine, socketPath);
if (!server.start()) {
fprintf(stderr, "Failed to start control server at %s\n", socketPath);
jack_deactivate(g_jackClient);
jack_client_close(g_jackClient);
return 1;
}
fprintf(stderr, "mixer-daemon running. Socket at %s. Press Ctrl+C to stop.\n", socketPath);
// --- Main loop: wait for shutdown ---
while (g_running) {
std::this_thread::sleep_for(std::chrono::milliseconds(200));
}
fprintf(stderr, "\nShutting down...\n");
// --- Cleanup ---
server.stop();
jack_deactivate(g_jackClient);
jack_client_close(g_jackClient);
fprintf(stderr, "mixer-daemon stopped.\n");
return 0;
}