// 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 #include using namespace pipedal; std::atomic 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(); } // 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(); }