DSPark 1.6.1
Header-only audio DSP framework in pure C++20 — zero dependencies
Loading...
Searching...
No Matches
StereoWidth.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
27#include "../Core/DspMath.h"
28#include "../Core/AudioSpec.h"
29#include "../Core/AudioBuffer.h"
30#include "../Core/StateBlob.h"
31
32#include <algorithm>
33#include <atomic>
34#include <cmath>
35#include <cstdint>
36#include <numbers>
37#include <vector>
38
39namespace dspark {
40
47template <FloatType T>
49{
50public:
51 StereoWidth() = default;
52 ~StereoWidth() = default; // Removed virtual to prevent vtable overhead
53
58 void prepare(double sampleRate) noexcept
59 {
60 if (!(sampleRate > 0.0)) return;
61 sampleRate_ = sampleRate;
62 updateBassMonoCoeff(bassMonoCutoff_.load(std::memory_order_relaxed));
63 reset();
64 }
65
67 void prepare(const AudioSpec& spec) noexcept { prepare(spec.sampleRate); }
68
73 void processBlock(AudioBufferView<T> buffer) noexcept
74 {
75 if (buffer.getNumChannels() >= 2)
76 process(buffer.getChannel(0), buffer.getChannel(1), buffer.getNumSamples());
77 }
78
85 void setWidth(T width) noexcept
86 {
87 if (!std::isfinite(width)) return;
88 width_.store(std::max(T(0), width), std::memory_order_relaxed);
89 }
90
92 [[nodiscard]] T getWidth() const noexcept { return width_.load(std::memory_order_relaxed); }
93
102 void setBassMono(bool enabled, double cutoffHz = 100.0) noexcept
103 {
104 if (cutoffHz > 0.0 && std::isfinite(cutoffHz))
105 {
106 bassMonoCutoff_.store(cutoffHz, std::memory_order_relaxed);
107 updateBassMonoCoeff(cutoffHz);
108 }
109 bassMonoEnabled_.store(enabled, std::memory_order_release);
110 }
111
118 void process(T* left, T* right, int numSamples) noexcept
119 {
120 // Load atomics once per block to allow tight loop vectorization
121 const T currentWidth = width_.load(std::memory_order_relaxed);
122 const bool bassMono = bassMonoEnabled_.load(std::memory_order_acquire);
123
124 if (bassMono)
125 {
126 const T coeff = bassMonoCoeff_.load(std::memory_order_relaxed);
127
128 // Bass-mono crossover: the side channel loses its lows through a
129 // one-pole high-pass; the mid passes UNTOUCHED. A one-pole split is
130 // complementary (LP + HP == 1 exactly), so no phase "compensation"
131 // of the mid is needed - the previously used first-order allpass
132 // rotated the mid by up to 180 degrees at HF, which swapped and
133 // inverted the channels above the transition band.
134 for (int i = 0; i < numSamples; ++i)
135 {
136 T l = left[i];
137 T r = right[i];
138
139 T mid = (l + r) * T(0.5);
140 T side = (l - r) * T(0.5) * currentWidth;
141
142 // Side processing (1-pole high-pass: side - LP(side))
143 T sideLpIn = side;
144 sideState_ += coeff * (sideLpIn - sideState_) + antiDenormal_;
145 sideState_ -= antiDenormal_; // Denormal flush
146 side = sideLpIn - sideState_;
147
148 left[i] = mid + side;
149 right[i] = mid - side;
150 }
151 }
152 else
153 {
154 // Fast-path branch: Pure Width control without filtering overhead
155 for (int i = 0; i < numSamples; ++i)
156 {
157 T mid = (left[i] + right[i]) * T(0.5);
158 T side = (left[i] - right[i]) * T(0.5) * currentWidth;
159
160 left[i] = mid + side;
161 right[i] = mid - side;
162 }
163 }
164 }
165
167 void reset() noexcept
168 {
169 sideState_ = T(0);
170 }
171
172
174 [[nodiscard]] std::vector<uint8_t> getState() const
175 {
176 StateWriter w(stateId("WIDE"), 1);
177 w.write("width", width_.load(std::memory_order_relaxed));
178 w.write("bassMono", bassMonoEnabled_.load(std::memory_order_relaxed));
179 w.write("bassCutoff", static_cast<float>(bassMonoCutoff_.load(std::memory_order_relaxed)));
180 return w.blob();
181 }
182
184 bool setState(const uint8_t* data, size_t size)
185 {
186 StateReader r(data, size);
187 if (!r.isValid() || r.processorId() != stateId("WIDE")) return false;
188 setWidth(static_cast<T>(r.read("width", 1.0f)));
189 setBassMono(r.read("bassMono", false),
190 static_cast<double>(r.read("bassCutoff", 100.0f)));
191 return true;
192 }
193
194protected:
195 void updateBassMonoCoeff(double cutoff) noexcept
196 {
197 if (sampleRate_ > 0.0)
198 {
199 // Calculate 1-pole coeff. Stored atomically to prevent data races.
200 T coeff = static_cast<T>(1.0 - std::exp(-std::numbers::pi * 2.0 * cutoff / sampleRate_));
201 bassMonoCoeff_.store(coeff, std::memory_order_relaxed);
202 }
203 }
204
205private:
206 double sampleRate_ = 48000.0;
207
208 // Lock-free parameters
209 std::atomic<T> width_ { T(1) };
210 std::atomic<bool> bassMonoEnabled_ { false };
211 std::atomic<double> bassMonoCutoff_ { 100.0 };
212 std::atomic<T> bassMonoCoeff_ { T(0) };
213
214 // Filter states
215 T sideState_ = T(0);
216
217 // Anti-denormal DC offset (type generic)
218 static constexpr T antiDenormal_ = static_cast<T>(1e-15);
219};
220
221} // namespace dspark
Non-owning view over audio channel data.
Definition AudioBuffer.h:50
Tolerant reader: missing keys yield defaults, unknown keys are skipped.
Definition StateBlob.h:160
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
High-performance stereo image processor with phase-aligned bass mono.
Definition StereoWidth.h:49
void setWidth(T width) noexcept
Sets the overall stereo width factor.
Definition StereoWidth.h:85
void processBlock(AudioBufferView< T > buffer) noexcept
Processes an AudioBufferView in-place.
Definition StereoWidth.h:73
void process(T *left, T *right, int numSamples) noexcept
Process a full block of audio. Optimized for SIMD vectorization.
void prepare(const AudioSpec &spec) noexcept
Prepares from AudioSpec (unified API).
Definition StereoWidth.h:67
std::vector< uint8_t > getState() const
Serializes the parameter state (setup/UI threads; allocates).
T getWidth() const noexcept
Returns current width setting.
Definition StereoWidth.h:92
void prepare(double sampleRate) noexcept
Prepares the processor and resets internal states.
Definition StereoWidth.h:58
void setBassMono(bool enabled, double cutoffHz=100.0) noexcept
Toggles Bass Mono and updates the crossover frequency.
~StereoWidth()=default
void updateBassMonoCoeff(double cutoff) noexcept
bool setState(const uint8_t *data, size_t size)
Restores parameters from a blob (tolerant; rejects foreign ids).
void reset() noexcept
Clears the internal filter states to prevent artifact ringing.
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