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
+109
View File
@@ -0,0 +1,109 @@
// Copyright (c) 2026 Ourpad Network
// See LICENSE file in the project root for full license text.
#pragma once
#include <string>
#include <vector>
#include <memory>
#include <atomic>
#include <cstdint>
#include "mixer/Types.hpp"
namespace mixer {
/// An audio bus that accumulates contributions from multiple sources.
///
/// Buses form the mixing topology:
/// Channels → subgroups → master
/// Channels → aux sends → aux buses (monitor mixes)
/// Aux buses → FxReturn buses → subgroup or master
class MixerBus {
public:
MixerBus(int64_t id, MixerBusType type, const std::string& name, int channels = 2);
~MixerBus() = default;
/// Bus identity
int64_t id() const { return id_; }
MixerBusType type() const { return type_; }
const std::string& name() const { return name_; }
void setName(const std::string& name) { name_ = name; }
/// Channel count (1 = mono, 2 = stereo, N = multi-channel)
int channelCount() const { return channelCount_; }
/// --- Control surface (atomic for RT-safe writes) ---
/// Master volume in dB (-inf to +12.0)
float volume() const { return volume_.load(); }
void setVolume(float db);
/// Mute
bool mute() const { return mute_.load(); }
void setMute(bool mute);
/// --- Audio buffers ---
/// Allocate internal buffers. Must be called before processing.
void allocateBuffers(size_t maxFrames);
/// Get read/write pointer to internal buffer for a channel
float* buffer(int channel) {
if (channel >= 0 && channel < (int)buffers_.size())
return buffers_[channel].data();
return nullptr;
}
const float* buffer(int channel) const {
if (channel >= 0 && channel < (int)buffers_.size())
return buffers_[channel].data();
return nullptr;
}
/// Accumulate (sum) audio from a source into this bus with gain.
void accumulate(
const float* const* source,
uint32_t frames,
float gain,
int sourceChannels
);
/// Same as accumulate but for a single interleaved source buffer
void accumulateMono(
const float* source,
uint32_t frames,
float gain
);
/// Clear all bus buffers to zero (must be called at start of each cycle)
void clear();
/// Apply bus-level processing (volume, mute) to the internal mix.
void process(uint32_t frames);
/// VU meter values after processing
float vuLeft() const { return vuLeft_.load(); }
float vuRight() const { return vuRight_.load(); }
/// Max frames this bus can handle
size_t maxFrames() const { return maxFrames_; }
private:
int64_t id_;
MixerBusType type_;
std::string name_;
int channelCount_;
std::atomic<float> volume_{0.0f}; // dB
std::atomic<bool> mute_{false};
// Internal accumulation buffers [channel][sample]
std::vector<std::vector<float>> buffers_;
// VU tracking
std::atomic<float> vuLeft_{-96.0f};
std::atomic<float> vuRight_{-96.0f};
size_t maxFrames_ = 512;
};
} // namespace mixer
+168
View File
@@ -0,0 +1,168 @@
// Copyright (c) 2026 Ourpad Network
// See LICENSE file in the project root for full license text.
#pragma once
#include <memory>
#include <vector>
#include <atomic>
#include <string>
#include "mixer/Types.hpp"
namespace mixer {
/// A single channel strip in the mixer.
///
/// Each MixerChannelStrip wraps:
/// - Volume fader, pan pot, mute, solo
/// - High-pass filter on input
/// - Aux sends (pre/post fader)
/// - VU metering
///
/// The channel operates in the real-time audio thread.
/// Control changes are made from the non-realtime thread via atomic snapshots.
class MixerChannelStrip {
public:
MixerChannelStrip(int channelIndex);
~MixerChannelStrip();
// Disable copy
MixerChannelStrip(const MixerChannelStrip&) = delete;
MixerChannelStrip& operator=(const MixerChannelStrip&) = delete;
/// Channel identity
int channelIndex() const { return channelIndex_; }
int64_t instanceId() const { return instanceId_; }
/// --- Control surface (thread-safe via atomics for simple values) ---
/// Volume in dB (-inf to +12.0)
float volume() const { return volume_; }
void setVolume(float db);
/// Pan: -1.0 (full left) to +1.0 (full right). 0.0 = center.
float pan() const { return pan_; }
void setPan(float pan);
/// Mute
bool mute() const { return mute_; }
void setMute(bool mute);
/// Solo — overrides mute for monitoring
bool solo() const { return solo_; }
void setSolo(bool solo);
/// Channel type for UI classification
MixerChannelType channelType() const { return channelType_; }
void setChannelType(MixerChannelType type) { channelType_ = type; }
/// User-assignable label
const std::string& label() const { return label_; }
void setLabel(const std::string& label) { label_ = label; }
/// --- Input processing ---
/// High-pass filter
bool hpEnabled() const { return hpEnabled_; }
void setHpEnabled(bool enabled) { hpEnabled_ = enabled; }
float hpFrequency() const { return hpFrequency_; }
void setHpFrequency(float freq) { hpFrequency_ = freq; }
/// --- Aux Sends ---
void setAuxSend(int index, const AuxSendConfig& config);
const AuxSendConfig& auxSend(int index) const;
size_t auxSendCount() const { return auxSends_.size(); }
void resizeAuxSends(size_t count);
/// --- Audio Processing (real-time thread) ---
/// Set sample rate and max buffer size
void setSampleRate(uint32_t sampleRate);
void setMaxBufferSize(size_t frames);
/// Allocate internal processing buffers. Must be called before process().
void allocateBuffers();
/// Process one audio block through this channel strip.
/// Reads from input, runs HPF, applies volume/pan.
/// Output goes to provided output buffer(s).
void process(
const float* const* inputBuffers,
size_t inputChannels,
float* const* outputBuffers,
size_t outputChannels,
uint32_t frames
);
/// Get post-FX, pre-fader audio for aux send calculation
const float* postFxBuffer(int channel) const {
if (channel < (int)postFxBuffers_.size()) return postFxBuffers_[channel].data();
return nullptr;
}
/// Get pre-FX audio for pre-fader aux sends
const float* preFxBuffer(int channel) const {
if (channel < (int)preFxBuffers_.size()) return preFxBuffers_[channel].data();
return nullptr;
}
/// Calculate aux send level considering pre/post fader and mute
float effectiveAuxLevel(int auxIndex, bool anySoloActive) const;
/// VU meter values (peak, not RMS — computed during process)
float vuLeft() const { return vuLeft_; }
float vuRight() const { return vuRight_; }
/// --- Lifecycle ---
void Activate();
void Deactivate();
void Unprepare();
private:
int channelIndex_;
int64_t instanceId_;
static std::atomic<int64_t> nextInstanceId_;
// Control values (atomic for RT-safe reads from control thread)
std::atomic<float> volume_{-96.0f}; // dB, -inf default
std::atomic<float> pan_{0.0f};
std::atomic<bool> mute_{false};
std::atomic<bool> solo_{false};
std::atomic<bool> hpEnabled_{false};
std::atomic<float> hpFrequency_{80.0f};
MixerChannelType channelType_ = MixerChannelType::Instrument;
std::string label_;
// Aux sends
std::vector<AuxSendConfig> auxSends_;
// Audio buffers (allocated at prepare time)
std::vector<std::vector<float>> preFxBuffers_; // Before FX chain (for pre-fader sends)
std::vector<std::vector<float>> postFxBuffers_; // After FX chain, before fader
// Sample rate / buffer size
uint32_t sampleRate_ = 48000;
size_t maxBufferSize_ = 512;
// VU tracking
float vuLeft_ = -96.0f;
float vuRight_ = -96.0f;
// Simple 1-pole HPF state (per channel)
struct HpfState {
float x1 = 0.0f, y1 = 0.0f;
};
std::vector<HpfState> hpfStates_;
// Apply pan law: constant power (-3dB center)
void applyPan(float& leftGain, float& rightGain) const;
// Apply HPF biquad to a buffer
void applyHpf(float* buffer, uint32_t frames, HpfState& state);
};
} // namespace mixer
+77
View File
@@ -0,0 +1,77 @@
// Copyright (c) 2026 Ourpad Network
// See LICENSE file in the project root for full license text.
#pragma once
#include <string>
#include <thread>
#include <atomic>
#include "mixer/MixerEngine.hpp"
namespace mixer {
/// Unix domain socket JSON control server for the MixerEngine.
///
/// Listens on a Unix domain socket (default: /tmp/mixer-daemon.sock).
/// Each client connection sends one JSON command per line and receives
/// one JSON response per line.
///
/// Supported commands:
/// {"cmd":"getState"} → full mixer state snapshot
/// {"cmd":"getVU"} → VU levels for all channels
/// {"cmd":"setChannelVolume","channel":N,"volume":-6.0}
/// {"cmd":"setChannelPan","channel":N,"pan":0.0}
/// {"cmd":"setChannelMute","channel":N,"mute":true}
/// {"cmd":"setChannelSolo","channel":N,"solo":false}
/// {"cmd":"setChannelLabel","channel":N,"label":"Guitar"}
/// {"cmd":"setBusVolume","busId":N,"volume":0.0}
/// {"cmd":"setBusMute","busId":N,"mute":false}
/// {"cmd":"routeChannelToBus","channel":N,"busId":N,"level":0.0}
/// {"cmd":"routeBusToBus","sourceBusId":N,"targetBusId":N,"level":0.0}
/// {"cmd":"removeRoute","sourceId":N,"targetBusId":N}
/// {"cmd":"autoCreateChannels","inputs":N,"outputs":N}
/// {"cmd":"addChannel","physicalInput":N} → {"status":"ok","channelIndex":N}
/// {"cmd":"removeChannel","channel":N}
class MixerControlServer {
public:
MixerControlServer(MixerEngine* engine, const std::string& socketPath = "/tmp/mixer-daemon.sock");
~MixerControlServer();
// Disable copy
MixerControlServer(const MixerControlServer&) = delete;
MixerControlServer& operator=(const MixerControlServer&) = delete;
/// Start the control server in a background thread.
/// Returns true if the socket was created successfully.
bool start();
/// Stop the control server and join the thread.
void stop();
/// True if the server is running.
bool running() const { return running_.load(); }
private:
MixerEngine* engine_;
std::string socketPath_;
int serverFd_ = -1;
std::thread serverThread_;
std::atomic<bool> running_{false};
// Server thread main loop
void serverLoop();
// Handle one client connection
void handleClient(int clientFd);
// Process a single JSON command and return JSON response
std::string processCommand(const std::string& commandJson);
// Helper: build full state JSON
std::string buildStateJson() const;
// Helper: build VU JSON
std::string buildVuJson() const;
};
} // namespace mixer
+210
View File
@@ -0,0 +1,210 @@
// Copyright (c) 2026 Ourpad Network
// See LICENSE file in the project root for full license text.
#pragma once
#include <memory>
#include <vector>
#include <map>
#include <atomic>
#include <cstdint>
#include <functional>
#include "mixer/MixerChannelStrip.hpp"
#include "mixer/MixerBus.hpp"
#include "mixer/Types.hpp"
namespace mixer {
class MixerChannelStrip;
class MixerBus;
/// The MixerEngine is the heart of the band-in-a-box digital mixer.
///
/// It owns and manages:
/// - N channel strips (MixerChannelStrip), one per physical/logical input
/// - M buses (MixerBus), including master, subgroups, aux sends
/// - A routing graph connecting channels to buses and buses to buses
///
/// Processing order per audio cycle:
/// 1. Clear all bus buffers
/// 2. For each channel: apply volume/pan → accumulate to routed buses
/// 3. For each aux send: calculate send level, accumulate to aux buses
/// 4. Route buses to buses according to routing matrix
/// 5. Process each bus (apply volume, compute VU)
/// 6. Master bus outputs are the final mix
///
/// All control methods are thread-safe for use from the non-RT thread.
/// The process() method runs in the RT audio thread.
class MixerEngine {
public:
MixerEngine();
~MixerEngine();
// Disable copy
MixerEngine(const MixerEngine&) = delete;
MixerEngine& operator=(const MixerEngine&) = delete;
/// --- Configuration ---
/// Set sample rate and max buffer size before preparation
void setSampleRate(uint32_t sampleRate);
void setMaxBufferSize(size_t frames);
/// --- Channel Management ---
/// Add a new channel strip for the given physical input index.
/// Returns a pointer to the new channel (valid until removed).
MixerChannelStrip* addChannel(int physicalInputIndex);
/// Auto-create channels based on the number of detected input channels.
/// Removes existing channels and creates one strip per input.
/// Also sets up default output routes.
void autoCreateChannels(uint32_t inputChannelCount, uint32_t outputChannelCount = 2);
/// Remove a channel by its channel index.
void removeChannel(int channelIndex);
/// Get a channel by index. Returns nullptr if not found.
MixerChannelStrip* getChannel(int channelIndex);
const MixerChannelStrip* getChannel(int channelIndex) const;
/// Number of channels currently in the mixer.
size_t channelCount() const { return channels_.size(); }
/// --- Bus Management ---
/// Add a new bus and return its ID.
int64_t addBus(MixerBusType type, const std::string& name, int channels = 2);
/// Remove a bus by ID.
void removeBus(int64_t busId);
/// Get a bus by ID. Returns nullptr if not found.
MixerBus* getBus(int64_t busId);
const MixerBus* getBus(int64_t busId) const;
/// Access the master bus (always present).
MixerBus* masterBus() { return masterBus_; }
const MixerBus* masterBus() const { return masterBus_; }
/// Get all bus IDs (for iteration).
std::vector<int64_t> busIds() const;
/// --- Routing ---
/// Route a channel to a bus with a given level in dB.
void routeChannelToBus(int channelIndex, int64_t busId, float levelDb = 0.0f);
/// Route a bus to another bus (e.g., subgroup to master).
void routeBusToBus(int64_t sourceBusId, int64_t targetBusId, float levelDb = 0.0f);
/// Remove a route.
void removeRoute(int64_t sourceId, int64_t targetBusId);
/// Clear all routes.
void clearRoutes();
/// --- Output Routing ---
/// Get the current output routing table (bus → physical output channel mapping).
const std::vector<MixerOutputRoute>& outputRoutes() const { return outputRoutes_; }
/// Set the entire output routing table.
void setOutputRoutes(const std::vector<MixerOutputRoute>& routes);
/// Add a single output route.
void addOutputRoute(int64_t busId, int sourceStartChannel, int targetStartChannel, int channels);
/// Remove all output routes for a given bus.
void removeOutputRoutes(int64_t busId);
/// Get all routes for a given bus.
std::vector<MixerOutputRoute> findOutputRoutesForBus(int64_t busId) const;
/// Get all current routes.
const std::vector<MixerRouteEntry>& routes() const { return routes_; }
/// --- Physical I/O Info ---
/// Number of physical input channels detected on the audio device.
uint32_t physicalInputCount() const { return physicalInputCount_; }
/// Number of physical output channels detected on the audio device.
uint32_t physicalOutputCount() const { return physicalOutputCount_; }
/// Set the physical I/O channel counts.
void setPhysicalChannelCounts(uint32_t inputs, uint32_t outputs) {
physicalInputCount_ = inputs;
physicalOutputCount_ = outputs;
}
/// --- Audio Processing (real-time thread) ---
/// Prepare all channels and allocate buffers.
void Prepare();
/// Activate all channels.
void Activate();
/// Deactivate all channels.
void Deactivate();
/// Process one full mixer cycle.
/// deviceInputs/outputs are the raw audio interface buffers.
void process(
float** deviceInputs,
uint32_t inputChannels,
float** deviceOutputs,
uint32_t outputChannels,
uint32_t frames
);
/// --- Solo Management ---
/// True if any channel has solo engaged.
bool anySoloActive() const;
/// --- State Serialization ---
MixerSnapshot captureSnapshot() const;
void applySnapshot(const MixerSnapshot& snapshot);
private:
std::vector<std::unique_ptr<MixerChannelStrip>> channels_;
std::map<int64_t, std::unique_ptr<MixerBus>> buses_;
MixerBus* masterBus_ = nullptr;
// Routing entries
std::vector<MixerRouteEntry> routes_;
// Output routing entries (bus → physical output channel mapping)
std::vector<MixerOutputRoute> outputRoutes_;
// Audio configuration
uint32_t sampleRate_ = 48000;
size_t maxBufferSize_ = 512;
// Next bus ID counter
static std::atomic<int64_t> nextBusId_;
// Temporary per-channel output buffers for routing
std::vector<std::vector<float>> channelOutputBuffers_;
// Physical I/O channel counts
uint32_t physicalInputCount_ = 0;
uint32_t physicalOutputCount_ = 0;
// Internal helper: accumulate a channel's output to all its routed buses
void routeChannelOutput(
MixerChannelStrip* channel,
float** channelOutput,
uint32_t frames
);
// Internal helper: process all bus-to-bus routing
void processBusRouting(uint32_t frames);
// Build a list of all routes from a given source
std::vector<MixerRouteEntry*> findRoutesForSource(int64_t sourceId);
};
} // namespace mixer
+90
View File
@@ -0,0 +1,90 @@
// Copyright (c) 2026 Ourpad Network
// See LICENSE file in the project root for full license text.
#pragma once
#include <cstdint>
#include <string>
#include <vector>
namespace mixer {
/// Types of buses in the mixer architecture.
enum class MixerBusType {
Master, // Main L/R output — end of signal chain
Subgroup, // Named subgroup (Drums, Guitars, Vocals...)
Aux, // Aux send bus (monitor mix or FX send)
FxReturn, // Stereo return from a shared FX processor (reverb, delay)
};
/// Channel type classification for mixer channel strips.
enum class MixerChannelType {
Instrument, // guitar, bass, keys — expects amp modeling
Mic, // vocal mic — expects compressor, EQ, reverb chain
Line, // line-level input (backing tracks, synths, drum machines)
AuxReturn, // return from external FX processor
};
/// Configuration for a single aux send on a channel strip.
struct AuxSendConfig {
float level = -96.0f; // dB, -96 ≈ -inf (effectively off)
bool preFader = false; // true = pre-fader (monitor send), false = post-fader (FX send)
int64_t targetBusId = -1; // bus ID this sends to
bool isActive() const { return level > -90.0f && targetBusId >= 0; }
};
/// Per-channel high-pass filter configuration.
struct HpfConfig {
bool enabled = false;
float frequency = 80.0f; // Hz
};
/// Routing entry: a source (channel or bus) feeds a target bus with a level.
struct MixerRouteEntry {
enum SourceType {
SourceChannel,
SourceBus
};
SourceType sourceType;
int64_t sourceId; // channel instanceId or bus ID
int64_t targetBusId; // the bus being fed into
float level = 0.0f; // dB
};
/// Describes a mapping from a mixer bus to physical output channels.
struct MixerOutputRoute {
int64_t sourceBusId; // Bus to route from
int sourceStartChannel; // Starting channel on the bus (0=L, 1=R)
int targetStartChannel; // Starting physical output channel
int channels; // Number of consecutive channels to route (1 or 2 typically)
};
/// A complete snapshot of the mixer state for save/restore.
struct MixerSnapshot {
struct ChannelState {
int channelIndex;
float volume;
float pan;
bool mute;
bool solo;
MixerChannelType channelType;
std::string label;
bool hpEnabled;
float hpFrequency;
std::vector<float> auxSendLevels; // indexed by aux bus index
};
struct BusState {
int64_t id;
std::string name;
MixerBusType type;
float volume;
bool mute;
};
std::vector<ChannelState> channels;
std::vector<BusState> buses;
std::vector<MixerRouteEntry> routes;
};
} // namespace mixer