DSPark 1.6.1
Header-only audio DSP framework in pure C++20 — zero dependencies
Loading...
Searching...
No Matches
MultibandCompressor.h
Go to the documentation of this file.
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
100 void processBlock(AudioBufferView<T> buffer) noexcept
101 {
102 if (!prepared_.load(std::memory_order_relaxed)) return;
103
104 // Clamp to the per-band buffers' geometry (allocated for the prepared
105 // spec): a wider or longer caller buffer must never index the band
106 // buffers out of bounds below.
107 const int nCh = std::min(buffer.getNumChannels(), bandBuffers_[0].getNumChannels());
108 const int nS = std::min(buffer.getNumSamples(), bandBuffers_[0].getNumSamples());
109 if (nCh <= 0 || nS <= 0) return;
110
111 // 1. Hand the crossover every band slot, truncated to this block. It
112 // returns how many bands it actually wrote THIS block (its band count
113 // is an atomic a concurrent setNumBands() may move between our read
114 // and its own); summing only what was written keeps stale band
115 // buffers out of the output.
116 for (int b = 0; b < MaxBands; ++b)
117 views_[b] = bandBuffers_[b].toView().getSubView(0, nS);
118
119 // 2. Split into bands
120 const int nb = crossover_.processBlock(buffer, views_.data(), MaxBands);
121 if (nb < 2) return; // crossover inactive: input left untouched
122
123 // 3. Bands (re-)enabled by a live band-count increase start clean:
124 // their compressors would otherwise replay the gain reduction from
125 // when they were last active (arbitrarily stale).
126 if (nb > lastNumBands_)
127 for (int b = lastNumBands_; b < nb; ++b)
128 compressors_[b].reset();
129 lastNumBands_ = nb;
130
131 // 4. Compress each band independently.
132 // IMPORTANT: bands are summed directly, so every band must share the
133 // SAME latency. Compressors default to 0 lookahead (latency 0) and a
134 // latency-free detector -> coherent sum. If you enable lookahead or
135 // the Hilbert detector (feed-forward adds its compensation delay),
136 // apply the SAME setting to every band via getBandCompressor(b); a
137 // divergent per-band latency would phase-cancel at the crossover
138 // regions (this class does not auto-delay-compensate).
139 for (int b = 0; b < nb; ++b)
140 compressors_[b].processBlock(views_[b]);
141
142 // 5. Sum bands back into the output buffer (SIMD add kernels)
143 for (int ch = 0; ch < nCh; ++ch)
144 {
145 T* const __restrict out = buffer.getChannel(ch);
146 const T* const __restrict src0 = bandBuffers_[0].getChannel(ch);
147
148 // Base copy (band 0)
149 std::copy(src0, src0 + nS, out);
150
151 // Accumulate remaining active bands
152 for (int b = 1; b < nb; ++b)
153 simd::add(out, bandBuffers_[b].getChannel(ch), nS);
154 }
155 }
156
157 // -- Configuration -------------------------------------------------------
158
171 void setNumBands(int n) noexcept
172 {
173 crossover_.setNumBands(std::clamp(n, 2, MaxBands));
174 }
175
181 void setCrossoverFrequency(int index, T freqHz) noexcept
182 {
183 crossover_.setCrossoverFrequency(index, freqHz);
184 }
185
190 void setOrder(int order) noexcept
191 {
192 crossover_.setOrder(order);
193 }
194
200 {
201 crossover_.setFilterMode(mode);
202 }
203
204 // -- Per-band compressor access ------------------------------------------
205
211 [[nodiscard]] Compressor<T>& getBandCompressor(int band) noexcept
212 {
213 assert(band >= 0 && band < MaxBands);
214 int safeBand = std::clamp(band, 0, MaxBands - 1);
215 return compressors_[safeBand];
216 }
217
223 [[nodiscard]] const Compressor<T>& getBandCompressor(int band) const noexcept
224 {
225 assert(band >= 0 && band < MaxBands);
226 int safeBand = std::clamp(band, 0, MaxBands - 1);
227 return compressors_[safeBand];
228 }
229
230 // -- Convenience per-band setters ----------------------------------------
231
237 void setBandThreshold(int band, T dB) noexcept
238 {
239 if (band >= 0 && band < MaxBands)
240 compressors_[band].setThreshold(dB);
241 }
242
248 void setBandRatio(int band, T ratio) noexcept
249 {
250 if (band >= 0 && band < MaxBands)
251 compressors_[band].setRatio(ratio);
252 }
253
259 void setBandAttack(int band, T ms) noexcept
260 {
261 if (band >= 0 && band < MaxBands)
262 compressors_[band].setAttack(ms);
263 }
264
270 void setBandRelease(int band, T ms) noexcept
271 {
272 if (band >= 0 && band < MaxBands)
273 compressors_[band].setRelease(ms);
274 }
275
276 // -- Queries -------------------------------------------------------------
277
287 [[nodiscard]] T getBandGainReductionDb(int band) const noexcept
288 {
289 if (band < 0 || band >= MaxBands) return T(0);
290 return compressors_[band].getGainReductionDb();
291 }
292
294 [[nodiscard]] int getNumBands() const noexcept { return crossover_.getNumBands(); }
295
297 [[nodiscard]] T getCrossoverFrequency(int index) const noexcept
298 {
299 return crossover_.getCrossoverFrequency(index);
300 }
301
303 [[nodiscard]] int getOrder() const noexcept { return crossover_.getOrder(); }
304
306 [[nodiscard]] typename CrossoverFilter<T, MaxBands>::FilterMode getCrossoverMode() const noexcept
307 {
308 return crossover_.getFilterMode();
309 }
310
312 [[nodiscard]] int getLatency() const noexcept
313 {
314 // Crossover latency + the largest per-band compressor latency (bands
315 // are expected to share the same lookahead/detector; see processBlock()).
316 int maxBand = 0;
317 const int nb = crossover_.getNumBands();
318 for (int b = 0; b < nb && b < MaxBands; ++b)
319 maxBand = std::max(maxBand, compressors_[b].getLatency());
320 return crossover_.getLatency() + maxBand;
321 }
322
324 void reset() noexcept
325 {
326 crossover_.reset();
327 for (auto& c : compressors_) c.reset();
328 lastNumBands_ = MaxBands;
329 }
330
332 [[nodiscard]] std::vector<uint8_t> getState() const
333 {
334 StateWriter w(stateId("MBCP"), 1);
335 const int n = crossover_.getNumBands();
336 w.write("numBands", n);
337 w.write("order", crossover_.getOrder());
338 w.write("xoverMode", static_cast<int32_t>(crossover_.getFilterMode()));
339 char key[24];
340 for (int i = 0; i < n - 1; ++i)
341 {
342 std::snprintf(key, sizeof(key), "x%d", i);
343 w.write(key, static_cast<float>(crossover_.getCrossoverFrequency(i)));
344 }
345 for (int i = 0; i < n; ++i)
346 {
347 std::snprintf(key, sizeof(key), "band%d", i);
348 w.write(key, getBandCompressor(i).getState());
349 }
350 return w.blob();
351 }
352
354 bool setState(const uint8_t* data, size_t size)
355 {
356 StateReader r(data, size);
357 if (!r.isValid() || r.processorId() != stateId("MBCP")) return false;
358 const int n = std::clamp(r.read("numBands", 3), 2, MaxBands);
359 setNumBands(n);
360 setOrder(r.read("order", 24));
362 r.read("xoverMode", 0)));
363 char key[24];
364 for (int i = 0; i < n - 1; ++i)
365 {
366 std::snprintf(key, sizeof(key), "x%d", i);
367 const float f = r.read(key, -1.0f);
368 if (f > 0.0f) setCrossoverFrequency(i, static_cast<T>(f));
369 }
370 for (int i = 0; i < n; ++i)
371 {
372 std::snprintf(key, sizeof(key), "band%d", i);
373 const auto nested = r.readBlob(key);
374 if (!nested.empty())
375 getBandCompressor(i).setState(nested.data(), nested.size());
376 }
377 return true;
378 }
379
380private:
381 std::atomic<bool> prepared_ { false };
382 int lastNumBands_ = MaxBands;
383 CrossoverFilter<T, MaxBands> crossover_;
384 std::array<Compressor<T>, MaxBands> compressors_ {};
385 std::array<AudioBuffer<T>, MaxBands> bandBuffers_ {};
386 std::array<AudioBufferView<T>, MaxBands> views_ {};
387};
388
389} // namespace dspark
Modular dynamic range compressor with analog-modeled ballistics and Hilbert detection.
Linkwitz-Riley crossover filter for multi-band audio processing.
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:160
std::vector< uint8_t > readBlob(const char *key) const
Reads a nested blob; empty when the key is absent.
Definition StateBlob.h:231
float read(const char *key, float defaultValue) const
Reads a float, or defaultValue when the key is absent.
Definition StateBlob.h:203
bool isValid() const noexcept
Definition StateBlob.h:198
uint32_t processorId() const noexcept
Definition StateBlob.h:199
Serializes key/value parameters into a versioned blob.
Definition StateBlob.h:52
std::vector< uint8_t > blob() const
Finalizes and returns the blob.
Definition StateBlob.h:104
void write(const char *key, float value)
Writes a float parameter.
Definition StateBlob.h:70
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:639
Describes the audio environment for a DSP processor.
Definition AudioSpec.h:35
constexpr bool isValid() const noexcept
Checks if the specification contains valid, processable parameters.
Definition AudioSpec.h:67
int numChannels
Number of audio channels (e.g., 1 = mono, 2 = stereo).
Definition AudioSpec.h:56
int maxBlockSize
Maximum number of samples per processing block.
Definition AudioSpec.h:51