diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 441677f..53a6601 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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} ) diff --git a/src/MixerBus.cpp b/src/MixerBus.cpp new file mode 100644 index 0000000..66912ea --- /dev/null +++ b/src/MixerBus.cpp @@ -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 +#include +#include + +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; + } +} diff --git a/src/MixerBus.hpp b/src/MixerBus.hpp new file mode 100644 index 0000000..4e080d5 --- /dev/null +++ b/src/MixerBus.hpp @@ -0,0 +1,124 @@ +// Copyright (c) 2026 Ourpad Network +// See LICENSE file in the project root for full license text. + +#pragma once + +#include +#include +#include +#include +#include + +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 volume_{0.0f}; // dB + std::atomic mute_{false}; + + // Internal accumulation buffers [channel][sample] + std::vector> buffers_; + + // VU tracking + std::atomic vuLeft_{-96.0f}; + std::atomic vuRight_{-96.0f}; + + size_t maxFrames_ = 512; +}; + +} // namespace pipedal diff --git a/src/MixerChannelStrip.cpp b/src/MixerChannelStrip.cpp new file mode 100644 index 0000000..687ab41 --- /dev/null +++ b/src/MixerChannelStrip.cpp @@ -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 +#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(); +} diff --git a/src/MixerChannelStrip.hpp b/src/MixerChannelStrip.hpp new file mode 100644 index 0000000..44a607f --- /dev/null +++ b/src/MixerChannelStrip.hpp @@ -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 +#include +#include +#include + +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 nextInstanceId_; + + // Control values (atomic for RT-safe reads from control thread) + std::atomic volume_{-96.0f}; // dB, -inf default + std::atomic pan_{0.0f}; + std::atomic mute_{false}; + std::atomic solo_{false}; + std::atomic hpEnabled_{false}; + std::atomic hpFrequency_{80.0f}; + + MixerChannelType channelType_ = MixerChannelType::Instrument; + std::string label_; + + // FX chain + Pedalboard fxChain_; + std::unique_ptr fxProcessor_; + + // Aux sends + std::vector auxSends_; + + // Audio buffers (allocated at prepare time) + std::vector> preFxBuffers_; // Before FX chain (for pre-fader sends) + std::vector> 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 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 diff --git a/src/MixerEngine.cpp b/src/MixerEngine.cpp new file mode 100644 index 0000000..cfaea0b --- /dev/null +++ b/src/MixerEngine.cpp @@ -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 +#include + +using namespace pipedal; + +std::atomic MixerEngine::nextBusId_{1}; + +MixerEngine::MixerEngine() +{ + // Create the master bus by default + int64_t masterId = nextBusId_++; + auto master = std::make_unique(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(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(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 MixerEngine::busIds() const +{ + std::vector 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 MixerEngine::findRoutesForSource(int64_t sourceId) +{ + std::vector 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 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; +} diff --git a/src/MixerEngine.hpp b/src/MixerEngine.hpp new file mode 100644 index 0000000..cf385a0 --- /dev/null +++ b/src/MixerEngine.hpp @@ -0,0 +1,212 @@ +// Copyright (c) 2026 Ourpad Network +// See LICENSE file in the project root for full license text. + +#pragma once + +#include +#include +#include +#include +#include +#include +#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 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& 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 auxSendLevels; // indexed by aux bus index + }; + struct BusState { + int64_t id; + std::string name; + MixerBusType type; + float volume; + bool mute; + }; + std::vector channels; + std::vector buses; + std::vector routes; + }; + + MixerSnapshot captureSnapshot() const; + void applySnapshot(const MixerSnapshot& snapshot); + +private: + std::vector> channels_; + std::map> buses_; + MixerBus* masterBus_ = nullptr; + + // Routing entries + std::vector 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 nextBusId_; + + // Temporary per-channel output buffers for routing + // Allocated once at prepare time + std::vector> 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 findRoutesForSource(int64_t sourceId); +}; + +} // namespace pipedal