DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
Loading...
Searching...
No Matches
MultibandCompressor.h
1// DSPark - Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi - MIT License
3
4#pragma once
5
31#include "CrossoverFilter.h"
32#include "Compressor.h"
33#include "../Core/AudioBuffer.h"
34#include "../Core/AudioSpec.h"
35#include "../Core/DspMath.h"
36#include "../Core/SimdOps.h"
37#include "../Core/StateBlob.h"
38
39#include <algorithm>
40#include <array>
41#include <atomic>
42#include <cassert>
43#include <cstddef>
44#include <cstdint>
45#include <cstdio>
46#include <vector>
47
48namespace dspark {
49
57template <FloatType T, int MaxBands = 12>
59{
60 static_assert(MaxBands >= 2, "MultibandCompressor needs at least 2 bands (one split)");
61
62public:
63 // -- Lifecycle -----------------------------------------------------------
64
74 void prepare(const AudioSpec& spec)
75 {
76 if (!spec.isValid()) return;
77
78 prepared_.store(false, std::memory_order_relaxed);
79
80 crossover_.prepare(spec);
81 for (int b = 0; b < MaxBands; ++b)
82 {
83 bandBuffers_[b].resize(spec.numChannels, spec.maxBlockSize);
84 compressors_[b].prepare(spec);
85 }
86 lastNumBands_ = MaxBands; // freshly prepared bands are all clean
87
88 prepared_.store(true, std::memory_order_relaxed);
89 }
90
101 void processBlock(AudioBufferView<T> buffer) noexcept
102 {
103 if (!prepared_.load(std::memory_order_relaxed)) return;
104
105 const int total = buffer.getNumSamples();
106 const int chunk = bandBuffers_[0].getNumSamples();
107 if (chunk <= 0) return;
108 for (int offset = 0; offset < total; offset += chunk)
109 processChunk(buffer.getSubView(offset, std::min(chunk, total - offset)));
110 }
111
112private:
114 void processChunk(AudioBufferView<T> buffer) noexcept
115 {
116 // Clamp to the per-band buffers' geometry (allocated for the prepared
117 // spec): a wider caller buffer must never index the band buffers out
118 // of bounds below.
119 const int nCh = std::min(buffer.getNumChannels(), bandBuffers_[0].getNumChannels());
120 const int nS = std::min(buffer.getNumSamples(), bandBuffers_[0].getNumSamples());
121 if (nCh <= 0 || nS <= 0) return;
122
123 // 1. Hand the crossover every band slot, truncated to this block. It
124 // returns how many bands it actually wrote THIS block (its band count
125 // is an atomic a concurrent setNumBands() may move between our read
126 // and its own); summing only what was written keeps stale band
127 // buffers out of the output.
128 for (int b = 0; b < MaxBands; ++b)
129 views_[b] = bandBuffers_[b].toView().getSubView(0, nS);
130
131 // 2. Split into bands
132 const int nb = crossover_.processBlock(buffer, views_.data(), MaxBands);
133 if (nb < 2) return; // crossover inactive: input left untouched
134
135 // 3. Bands (re-)enabled by a live band-count increase start clean:
136 // their compressors would otherwise replay the gain reduction from
137 // when they were last active (arbitrarily stale).
138 if (nb > lastNumBands_)
139 for (int b = lastNumBands_; b < nb; ++b)
140 compressors_[b].reset();
141 lastNumBands_ = nb;
142
143 // 4. Compress each band independently.
144 // IMPORTANT: bands are summed directly, so every band must share the
145 // SAME latency. Compressors default to 0 lookahead (latency 0) and a
146 // latency-free detector -> coherent sum. If you enable lookahead or
147 // the Hilbert detector (feed-forward adds its compensation delay),
148 // apply the SAME setting to every band via getBandCompressor(b); a
149 // divergent per-band latency would phase-cancel at the crossover
150 // regions (this class does not auto-delay-compensate).
151 for (int b = 0; b < nb; ++b)
152 compressors_[b].processBlock(views_[b]);
153
154 // 5. Sum bands back into the output buffer (SIMD add kernels)
155 for (int ch = 0; ch < nCh; ++ch)
156 {
157 T* const __restrict out = buffer.getChannel(ch);
158 const T* const __restrict src0 = bandBuffers_[0].getChannel(ch);
159
160 // Base copy (band 0)
161 std::copy(src0, src0 + nS, out);
162
163 // Accumulate remaining active bands
164 for (int b = 1; b < nb; ++b)
165 simd::add(out, bandBuffers_[b].getChannel(ch), nS);
166 }
167 }
168
169public:
170 // -- Configuration -------------------------------------------------------
171
184 void setNumBands(int n) noexcept
185 {
186 crossover_.setNumBands(std::clamp(n, 2, MaxBands));
187 }
188
194 void setCrossoverFrequency(int index, T freqHz) noexcept
195 {
196 crossover_.setCrossoverFrequency(index, freqHz);
197 }
198
203 void setOrder(int order) noexcept
204 {
205 crossover_.setOrder(order);
206 }
207
213 {
214 crossover_.setFilterMode(mode);
215 }
216
217 // -- Per-band compressor access ------------------------------------------
218
224 [[nodiscard]] Compressor<T>& getBandCompressor(int band) noexcept
225 {
226 assert(band >= 0 && band < MaxBands);
227 int safeBand = std::clamp(band, 0, MaxBands - 1);
228 return compressors_[safeBand];
229 }
230
236 [[nodiscard]] const Compressor<T>& getBandCompressor(int band) const noexcept
237 {
238 assert(band >= 0 && band < MaxBands);
239 int safeBand = std::clamp(band, 0, MaxBands - 1);
240 return compressors_[safeBand];
241 }
242
243 // -- Convenience per-band setters ----------------------------------------
244
250 void setBandThreshold(int band, T dB) noexcept
251 {
252 if (band >= 0 && band < MaxBands)
253 compressors_[band].setThreshold(dB);
254 }
255
261 void setBandRatio(int band, T ratio) noexcept
262 {
263 if (band >= 0 && band < MaxBands)
264 compressors_[band].setRatio(ratio);
265 }
266
272 void setBandAttack(int band, T ms) noexcept
273 {
274 if (band >= 0 && band < MaxBands)
275 compressors_[band].setAttack(ms);
276 }
277
283 void setBandRelease(int band, T ms) noexcept
284 {
285 if (band >= 0 && band < MaxBands)
286 compressors_[band].setRelease(ms);
287 }
288
289 // -- Queries -------------------------------------------------------------
290
300 [[nodiscard]] T getBandGainReductionDb(int band) const noexcept
301 {
302 if (band < 0 || band >= MaxBands) return T(0);
303 return compressors_[band].getGainReductionDb();
304 }
305
307 [[nodiscard]] int getNumBands() const noexcept { return crossover_.getNumBands(); }
308
310 [[nodiscard]] T getCrossoverFrequency(int index) const noexcept
311 {
312 return crossover_.getCrossoverFrequency(index);
313 }
314
316 [[nodiscard]] int getOrder() const noexcept { return crossover_.getOrder(); }
317
319 [[nodiscard]] typename CrossoverFilter<T, MaxBands>::FilterMode getCrossoverMode() const noexcept
320 {
321 return crossover_.getFilterMode();
322 }
323
325 [[nodiscard]] int getLatency() const noexcept
326 {
327 // Crossover latency + the largest per-band compressor latency (bands
328 // are expected to share the same lookahead/detector; see processBlock()).
329 int maxBand = 0;
330 const int nb = crossover_.getNumBands();
331 for (int b = 0; b < nb && b < MaxBands; ++b)
332 maxBand = std::max(maxBand, compressors_[b].getLatency());
333 return crossover_.getLatency() + maxBand;
334 }
335
337 void reset() noexcept
338 {
339 crossover_.reset();
340 for (auto& c : compressors_) c.reset();
341 lastNumBands_ = MaxBands;
342 }
343
345 [[nodiscard]] std::vector<uint8_t> getState() const
346 {
347 StateWriter w(stateId("MBCP"), 1);
348 const int n = crossover_.getNumBands();
349 w.write("numBands", n);
350 w.write("order", crossover_.getOrder());
351 w.write("xoverMode", static_cast<int32_t>(crossover_.getFilterMode()));
352 char key[24];
353 for (int i = 0; i < n - 1; ++i)
354 {
355 std::snprintf(key, sizeof(key), "x%d", i);
356 w.write(key, static_cast<float>(crossover_.getCrossoverFrequency(i)));
357 }
358 for (int i = 0; i < n; ++i)
359 {
360 std::snprintf(key, sizeof(key), "band%d", i);
361 w.write(key, getBandCompressor(i).getState());
362 }
363 return w.blob();
364 }
365
367 bool setState(const uint8_t* data, size_t size)
368 {
369 StateReader r(data, size);
370 if (!r.isValid() || r.processorId() != stateId("MBCP")) return false;
371 const int n = std::clamp(r.read("numBands", 3), 2, MaxBands);
372 setNumBands(n);
373 setOrder(r.read("order", 24));
375 r.read("xoverMode", 0)));
376 char key[24];
377 for (int i = 0; i < n - 1; ++i)
378 {
379 std::snprintf(key, sizeof(key), "x%d", i);
380 const float f = r.read(key, -1.0f);
381 if (f > 0.0f) setCrossoverFrequency(i, static_cast<T>(f));
382 }
383 for (int i = 0; i < n; ++i)
384 {
385 std::snprintf(key, sizeof(key), "band%d", i);
386 const auto nested = r.readBlob(key);
387 if (!nested.empty())
388 getBandCompressor(i).setState(nested.data(), nested.size());
389 }
390 return true;
391 }
392
393private:
394 std::atomic<bool> prepared_ { false };
395 int lastNumBands_ = MaxBands;
396 CrossoverFilter<T, MaxBands> crossover_;
397 std::array<Compressor<T>, MaxBands> compressors_ {};
398 std::array<AudioBuffer<T>, MaxBands> bandBuffers_ {};
399 std::array<AudioBufferView<T>, MaxBands> views_ {};
400};
401
402} // namespace dspark
Non-owning view over audio channel data.
Definition AudioBuffer.h:50
High-fidelity modular compressor designed for real-time applications.
Definition Compressor.h:88
FilterMode
Filter processing mode.
Multi-band compressor: crossover split, per-band compression, sum.
void setNumBands(int n) noexcept
Sets the number of active frequency bands.
void setBandRatio(int band, T ratio) noexcept
Sets the ratio for a specific band.
const Compressor< T > & getBandCompressor(int band) const noexcept
Direct constant access to a band's compressor for state queries.
void setOrder(int order) noexcept
Sets the crossover slope in dB/oct: 12, 24 or 48.
void processBlock(AudioBufferView< T > buffer) noexcept
Processes audio through the multi-band compressor.
bool setState(const uint8_t *data, size_t size)
Restores topology and band compressors from a blob.
void setCrossoverFrequency(int index, T freqHz) noexcept
Sets the crossover frequency for a specific split point.
std::vector< uint8_t > getState() const
Serializes crossover topology and per-band compressor states.
void setCrossoverMode(typename CrossoverFilter< T, MaxBands >::FilterMode mode) noexcept
Sets the phase/processing mode of the crossover (IIR or linear-phase).
void setBandThreshold(int band, T dB) noexcept
Sets the threshold for a specific band.
T getCrossoverFrequency(int index) const noexcept
Returns the target frequency of split point index in Hz.
int getLatency() const noexcept
Returns the total latency of the multi-band system.
int getNumBands() const noexcept
Returns the current number of active bands.
void setBandAttack(int band, T ms) noexcept
Sets the attack time for a specific band.
void prepare(const AudioSpec &spec)
Prepares the multiband compressor and internal buffers for processing.
T getBandGainReductionDb(int band) const noexcept
Gets the current gain reduction applied to a specific band.
Compressor< T > & getBandCompressor(int band) noexcept
Direct access to a band's compressor for full configuration.
void reset() noexcept
Resets all internal states (envelopes, delay lines, etc.).
CrossoverFilter< T, MaxBands >::FilterMode getCrossoverMode() const noexcept
Returns the crossover processing mode (IIR or linear-phase).
int getOrder() const noexcept
Returns the crossover slope in dB/oct (12, 24 or 48).
void setBandRelease(int band, T ms) noexcept
Sets the release time for a specific band.
Tolerant reader: missing keys yield defaults, unknown keys are skipped.
Definition StateBlob.h:161
std::vector< uint8_t > readBlob(const char *key) const
Reads a nested blob; empty when the key is absent.
Definition StateBlob.h:232
float read(const char *key, float defaultValue) const
Reads a float, or defaultValue when the key is absent.
Definition StateBlob.h:204
bool isValid() const noexcept
Definition StateBlob.h:199
uint32_t processorId() const noexcept
Definition StateBlob.h:200
Serializes key/value parameters into a versioned blob.
Definition StateBlob.h:53
std::vector< uint8_t > blob() const
Finalizes and returns the blob.
Definition StateBlob.h:105
void write(const char *key, float value)
Writes a float parameter.
Definition StateBlob.h:71
void add(float *DSPARK_RESTRICT dst, const float *DSPARK_RESTRICT src, int count) noexcept
Adds source samples into a destination buffer (no scaling).
Definition SimdOps.h:608
Main namespace for the DSPark framework.
constexpr uint32_t stateId(const char(&tag)[5]) noexcept
Builds a FOURCC processor id, e.g. dspark::stateId("COMP").
Definition StateBlob.h:651
Describes the audio environment for a DSP processor.
Definition AudioSpec.h:37
constexpr bool isValid() const noexcept
Checks if the specification contains valid, processable parameters.
Definition AudioSpec.h:71
int numChannels
Number of audio channels (e.g., 1 = mono, 2 = stereo).
Definition AudioSpec.h:58
int maxBlockSize
Maximum number of samples per processing block.
Definition AudioSpec.h:53