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:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user