df5a317ceb
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.
281 lines
8.2 KiB
C++
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();
|
|
}
|