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