Files
op-pedal/src/MixerChannelStrip.cpp
T
shawn df5a317ceb 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.
2026-06-20 13:57:15 -04:00

281 lines
8.2 KiB
C++

// 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();
}