feat: add MixerEngine core — ChannelStrip, MixerBus, and MixerEngine for band-in-a-box digital mixer

MixerEngine architecture:
- MixerChannelStrip: per-input FX chain (Lv2Pedalboard reuse),
  volume, pan, mute, solo, HPF, aux sends, VU metering
- MixerBus: accumulation bus with volume, mute, VU. Supports
  master, subgroup, aux, and FX-return bus types
- MixerEngine: orchestrator managing channel→bus routing graph,
  bus→bus routing, solo override, and the full real-time audio
  processing cycle

All new code compiles cleanly with the existing C++20 build and
follows the existing PiPedal codebase conventions (namespaces,
error handling, buffer patterns). CPU-efficient real-time thread
processing with atomic control surface interaction.
This commit is contained in:
2026-06-20 13:57:15 -04:00
parent e1014462b4
commit df5a317ceb
7 changed files with 1536 additions and 0 deletions
+5
View File
@@ -369,6 +369,11 @@ set (PIPEDAL_SOURCES
AudioDriver.hpp
AudioConfig.hpp
# Mixer Engine (Band-in-a-Box)
MixerChannelStrip.cpp MixerChannelStrip.hpp
MixerBus.cpp MixerBus.hpp
MixerEngine.cpp MixerEngine.hpp
${VST3_SOURCES}
)
+174
View File
@@ -0,0 +1,174 @@
// Copyright (c) 2026 Ourpad Network
// See LICENSE file in the project root for full license text.
#include "pch.h"
#include "MixerBus.hpp"
#include <cmath>
#include <algorithm>
#include <cstring>
using namespace pipedal;
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;
}
}
+124
View File
@@ -0,0 +1,124 @@
// 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>
namespace pipedal {
/// 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)
};
/// 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
///
/// Key design decisions:
/// - Buses are flat accumulators: they sum incoming audio with gain
/// - Bus processing is minimal (volume, mute only)
/// - A bus can be fed INTO another bus via the routing graph
/// - All audio is floating-point, 32-bit
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.
/// Performs: bus[ch][i] += source[ch][i] * gain for all channels
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.
/// Reads internal mix buffer, applies gain, writes back.
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 pipedal
+280
View File
@@ -0,0 +1,280 @@
// Copyright (c) 2026 Ourpad Network
// See LICENSE file in the project root for full license text.
#include "pch.h"
#include "MixerChannelStrip.hpp"
#include "Lv2Effect.hpp"
#include "PiPedalMath.hpp"
#include <algorithm>
#include <cmath>
using namespace pipedal;
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::prepareFx(IHost* pHost, Lv2PedalboardErrorList& errorList,
ExistingEffectMap* existingEffects)
{
// Create or re-create the Lv2Pedalboard for this channel's FX chain
if (!fxProcessor_) {
fxProcessor_ = std::make_unique<Lv2Pedalboard>();
}
// Allocate pre/post FX buffers (stereo, up to max buffer size)
preFxBuffers_.clear();
postFxBuffers_.clear();
for (int i = 0; i < 2; ++i) {
preFxBuffers_.emplace_back(maxBufferSize_, 0.0f);
postFxBuffers_.emplace_back(maxBufferSize_, 0.0f);
}
// Prepare the FX processor with this channel's pedalboard
fxProcessor_->Prepare(pHost, fxChain_, errorList, existingEffects);
fxProcessor_->Activate();
}
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
// sin/cos distribution: L = cos(pan * PI/4), R = sin(pan * PI/4)
// Normalized so center = -3dB each
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);
// Compensate for equal-power pan: center should sum to unity
// Already handled by sin/cos distribution
}
void MixerChannelStrip::applyHpf(float* buffer, uint32_t frames, HpfState& state)
{
if (!hpEnabled_) return;
// Simple 1st-order IIR HPF: y[n] = 0.5 * (x[n] - x[n-1] + y[n-1])
// Cutoff ~ 80Hz at 48kHz. For sharper roll-off, use biquad.
// This is intentionally simple for real-time safety.
float fc = hpFrequency_ / sampleRate_;
float alpha = fc / (fc + 0.5f); // approximation: R = 1/(2*PI*fc)
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: Run the FX chain (processes preFxBuffers_ -> postFxBuffers_)
if (fxProcessor_) {
// Build float* arrays for Lv2Pedalboard::Run
float* fxInputs[2];
float* fxOutputs[2];
for (int i = 0; i < 2; ++i) {
fxInputs[i] = i < (int)preFxBuffers_.size() ? preFxBuffers_[i].data() : nullptr;
fxOutputs[i] = i < (int)postFxBuffers_.size() ? postFxBuffers_[i].data() : nullptr;
}
// Run the FX chain (Lv2Pedalboard manages its internal routing)
fxProcessor_->Run(
(float**)fxInputs,
(float**)fxOutputs,
frames,
nullptr // no realtime ring buffer writer for now
);
} else {
// No FX chain — passthrough pre to post
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: releaseRate = exp(-1 / (0.3 * sampleRate / frames))
static const float releaseRate = 0.95f;
vu = vu * releaseRate + peakDb * (1.0f - releaseRate);
}
}
}
void MixerChannelStrip::Activate()
{
if (fxProcessor_) {
fxProcessor_->Activate();
}
}
void MixerChannelStrip::Deactivate()
{
if (fxProcessor_) {
fxProcessor_->Deactivate();
}
}
void MixerChannelStrip::Unprepare()
{
if (fxProcessor_) {
fxProcessor_->Deactivate();
fxProcessor_.reset();
}
preFxBuffers_.clear();
postFxBuffers_.clear();
}
+229
View File
@@ -0,0 +1,229 @@
// Copyright (c) 2026 Ourpad Network
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
// of the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#pragma once
#include "Pedalboard.hpp"
#include "Lv2Pedalboard.hpp"
#include "IEffect.hpp"
#include "BufferPool.hpp"
#include <memory>
#include <vector>
#include <atomic>
#include <string>
namespace pipedal {
/// Channel type classification for mixer channel strips.
enum class MixerChannelType {
Instrument, // guitar, bass, keys — expects NAM/guitar 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
// Filter state for simple biquad — sized for max buffer
// Allocated at prepare time
};
/// A single channel strip in the mixer.
///
/// Each MixerChannelStrip wraps:
/// - A mini-pedalboard (FX chain using existing Lv2Pedalboard)
/// - 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; }
/// --- FX Chain ---
/// Access the channel's pedalboard for plugin management
Pedalboard& fxChain() { return fxChain_; }
const Pedalboard& fxChain() const { return fxChain_; }
/// Get the real-time processor for this channel's FX chain
Lv2Pedalboard* fxProcessor() { return fxProcessor_.get(); }
/// Prepare the FX chain for processing
void prepareFx(IHost* pHost, Lv2PedalboardErrorList& errorList,
ExistingEffectMap* existingEffects = nullptr);
/// --- 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);
/// Process one audio block through this channel strip.
/// Reads from input, runs HPF → FX chain, applies volume/pan.
/// Output goes to provided output buffer(s).
/// Returns the number of processed samples.
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;
}
/// Calcuate 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_;
// FX chain
Pedalboard fxChain_;
std::unique_ptr<Lv2Pedalboard> fxProcessor_;
// 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
BufferPool bufferPool_;
// 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 pipedal
+512
View File
@@ -0,0 +1,512 @@
// Copyright (c) 2026 Ourpad Network
// See LICENSE file in the project root for full license text.
#include "pch.h"
#include "MixerEngine.hpp"
#include "MixerChannelStrip.hpp"
#include "MixerBus.hpp"
#include "Lv2Pedalboard.hpp"
#include "IHost.hpp"
#include <algorithm>
#include <cmath>
using namespace pipedal;
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));
// Default: route channel directly to master
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()
);
// Unprepare the channel
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(IHost* pHost, Lv2PedalboardErrorList& errorList)
{
pHost_ = pHost;
// 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
}
// Prepare each channel's FX chain
for (auto& channel : channels_) {
channel->setSampleRate(sampleRate_);
channel->setMaxBufferSize(maxBufferSize_);
channel->prepareFx(pHost, errorList, nullptr);
}
}
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);
// Check aux sends if this is an aux bus
// For standard bus routing, just accumulate
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
// This means we need access to the pre-fader buffer from the channel
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)
{
// Process bus-to-bus routes
// This is simple: for each bus route, accumulate source bus output to target bus
for (auto& route : routes_) {
if (route.sourceType != MixerRouteEntry::SourceBus) continue;
MixerBus* sourceBus = getBus(route.sourceId);
MixerBus* targetBus = getBus(route.targetBusId);
if (!sourceBus || !targetBus) continue;
// Build float* array from source bus
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();
for (size_t ch = 0; ch < numChannels; ++ch) {
auto* channel = channels_[ch].get();
// Build input buffer pointers for this channel
// Channel ch reads from device input ch (if available)
float* channelInputs[2] = { nullptr, nullptr };
if (ch < inputChannels) {
channelInputs[0] = deviceInputs[ch]; // mono input
// For stereo, pair consecutive channels: (0,1), (2,3), etc.
if (ch + 1 < inputChannels) {
channelInputs[1] = deviceInputs[ch + 1];
}
}
// 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_;
}
// Process the channel strip
channel->process(
(const float* const*)channelInputs,
std::min((size_t)2, (size_t)inputChannels),
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 master bus to device outputs
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) {
// Mono to stereo: copy L to R if R bus channel doesn't exist
const float* srcL = masterBus_->buffer(0);
if (srcL) {
std::copy(srcL, srcL + frames, deviceOutputs[outCh]);
}
}
}
}
}
// --- State Serialization ---
MixerEngine::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;
}
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;
}
+212
View File
@@ -0,0 +1,212 @@
// 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 "MixerChannelStrip.hpp"
#include "MixerBus.hpp"
namespace pipedal {
class MixerChannelStrip;
class MixerBus;
class Lv2PedalboardErrorList;
class IHost;
/// 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
};
/// 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: process FX chain → 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);
/// 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();
/// Get all current routes.
const std::vector<MixerRouteEntry>& routes() const { return routes_; }
/// --- Audio Processing (real-time thread) ---
/// Prepare all channels and allocate buffers.
void Prepare(IHost* pHost, Lv2PedalboardErrorList& errorList);
/// Activate all channels.
void Activate();
/// Deactivate all channels.
void Deactivate();
/// Process one full mixer cycle.
/// deviceInputs/outputs are the raw audio interface buffers.
/// The mixer reads from inputs, processes through channels → buses, writes to outputs.
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 ---
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;
};
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_;
// Audio configuration
uint32_t sampleRate_ = 48000;
size_t maxBufferSize_ = 512;
// IHost reference for FX preparation
IHost* pHost_ = nullptr;
// Next bus ID counter
static std::atomic<int64_t> nextBusId_;
// Temporary per-channel output buffers for routing
// Allocated once at prepare time
std::vector<std::vector<float>> channelOutputBuffers_;
// 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 pipedal