Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
expander.hpp
Go to the documentation of this file.
1#pragma once
2
5
8
9#include <algorithm>
10#include <array>
11#include <cmath>
12#include <limits>
13
14namespace pulp::signal {
15
18
27template <typename SampleType = float> class ExpanderT {
28 public:
30
31 struct Config {
33 SampleType threshold_db = SampleType{-40};
34 SampleType ratio = SampleType{2};
35 SampleType range_db = SampleType{24};
36 SampleType knee_db = SampleType{6};
37 SampleType attack_ms = SampleType{5};
38 SampleType release_ms = SampleType{100};
39 DetectorMode detector = DetectorMode::rms;
41 };
42
43 static constexpr SampleType kMinThresholdDb = SampleType{-160};
44 static constexpr SampleType kMaxThresholdDb = SampleType{24};
45 static constexpr SampleType kMinRatio = SampleType{1};
46 static constexpr SampleType kMaxRatio = SampleType{20};
47 static constexpr SampleType kMaxRangeDb = SampleType{96};
48 static constexpr SampleType kMaxKneeDb = SampleType{48};
49 static constexpr SampleType kMaxSampleRate = SampleType{1536000};
50
51 [[nodiscard]] ExpanderStatus prepare(SampleType sample_rate) noexcept {
52 if (!finite(sample_rate) || !(sample_rate > SampleType{0}) || sample_rate > kMaxSampleRate)
54 sample_rate_ = sample_rate;
55 apply_config(config_);
56 prepared_ = true;
57 reset();
59 }
60
63 [[nodiscard]] ExpanderStatus configure(const Config& next) noexcept {
64 if (!valid(next))
66 const bool detector_topology_changed =
67 next.detector != config_.detector || next.stereo_link != config_.stereo_link;
68 config_ = next;
69 if (prepared_) {
70 apply_config(config_);
71 if (detector_topology_changed)
72 reset();
73 }
75 }
76
78 [[nodiscard]] static SampleType gain_computer_db(SampleType level_db,
79 const Config& config) noexcept {
80 if (!finite(level_db) || !valid(config))
81 return SampleType{0};
82 const SampleType x = level_db - config.threshold_db;
83 const SampleType slope = config.ratio - SampleType{1};
84 SampleType gain{};
85 if (config.mode == ExpansionMode::downward) {
86 if (slope > SampleType{0} && x <= -config.range_db / slope)
87 return -config.range_db;
88 if (!(config.knee_db > SampleType{0})) {
89 gain = x < SampleType{0} ? slope * x : SampleType{0};
90 } else {
91 const SampleType half = config.knee_db / SampleType{2};
92 if (x <= -half) {
93 gain = slope * x;
94 } else if (x < half) {
95 const SampleType distance = half - x;
96 gain = -slope * distance * distance / (SampleType{2} * config.knee_db);
97 }
98 }
99 return std::max(-config.range_db, gain);
100 }
101
102 if (slope > SampleType{0} && x >= config.range_db / slope)
103 return config.range_db;
104 if (!(config.knee_db > SampleType{0})) {
105 gain = x > SampleType{0} ? slope * x : SampleType{0};
106 } else {
107 const SampleType half = config.knee_db / SampleType{2};
108 if (x >= half) {
109 gain = slope * x;
110 } else if (x > -half) {
111 const SampleType distance = x + half;
112 gain = slope * distance * distance / (SampleType{2} * config.knee_db);
113 }
114 }
115 return std::min(config.range_db, gain);
116 }
117
118 std::array<SampleType, 2> process(SampleType left, SampleType right) noexcept {
119 if (!prepared_ || !finite(left) || !finite(right)) {
120 reset();
121 return {SampleType{0}, SampleType{0}};
122 }
123
124 const auto envelope = process_detector(left, right);
125 std::array<SampleType, 2> output{};
126 for (std::size_t channel = 0; channel < 2; ++channel) {
127 if (envelope[channel] > SampleType{0}) {
128 const SampleType level = SampleType{20} * std::log10(envelope[channel]);
129 current_gain_db_[channel] = gain_computer_db(level, config_);
130 } else {
131 current_gain_db_[channel] =
132 config_.mode == ExpansionMode::downward && config_.ratio > SampleType{1}
133 ? -config_.range_db
134 : SampleType{0};
135 }
136 output[channel] = bounded_multiply(channel == 0 ? left : right,
137 db_to_linear(current_gain_db_[channel]));
138 }
139 if (bypassed_)
140 return {left, right};
141 return output;
142 }
143
144 void process(SampleType* left, SampleType* right, int num_samples) noexcept {
145 if (num_samples <= 0 || left == nullptr || right == nullptr)
146 return;
147 for (int i = 0; i < num_samples; ++i) {
148 const auto output = process(left[i], right[i]);
149 left[i] = output[0];
150 right[i] = output[1];
151 }
152 }
153
154 void set_bypassed(bool bypassed) noexcept {
155 bypassed_ = bypassed;
156 }
157 [[nodiscard]] bool bypassed() const noexcept {
158 return bypassed_;
159 }
160
161 void reset() noexcept {
162 detector_state_ = {};
163 current_gain_db_ = {};
164 }
165
166 [[nodiscard]] const Config& config() const noexcept {
167 return config_;
168 }
169 [[nodiscard]] bool prepared() const noexcept {
170 return prepared_;
171 }
172 [[nodiscard]] SampleType sample_rate() const noexcept {
173 return sample_rate_;
174 }
175 [[nodiscard]] int latency_samples() const noexcept {
176 return 0;
177 }
178 [[nodiscard]] int tail_samples() const noexcept {
179 return 0;
180 }
181 [[nodiscard]] std::array<SampleType, 2> current_gain_db() const noexcept {
182 return current_gain_db_;
183 }
184 [[nodiscard]] std::array<GainReduction, 2> gain_reduction() const noexcept {
185 return {GainReduction::from_signed_db(current_gain_db_[0]),
186 GainReduction::from_signed_db(current_gain_db_[1])};
187 }
188
189 private:
190 static bool finite(SampleType value) noexcept {
191 return std::isfinite(static_cast<double>(value));
192 }
193
194 static bool valid(const Config& config) noexcept {
199 config.range_db >= SampleType{0} && config.range_db <= kMaxRangeDb &&
200 finite(config.knee_db) && config.knee_db >= SampleType{0} &&
201 config.knee_db <= kMaxKneeDb && finite(config.attack_ms) &&
202 config.attack_ms >= SampleType{0.01} && config.attack_ms <= SampleType{2000} &&
203 finite(config.release_ms) && config.release_ms >= SampleType{0.01} &&
204 config.release_ms <= SampleType{10000} &&
205 (config.detector == DetectorMode::peak || config.detector == DetectorMode::rms) &&
206 (config.stereo_link == DynamicsStereoLink::independent ||
208 }
209
210 static SampleType db_to_linear(SampleType db) noexcept {
211 return std::pow(SampleType{10}, db / SampleType{20});
212 }
213
214 static SampleType bounded_multiply(SampleType input, SampleType gain) noexcept {
215 const SampleType limit = std::numeric_limits<SampleType>::max();
216 if (gain > SampleType{1} && std::abs(input) > limit / gain)
217 return std::copysign(limit, input);
218 return snap_to_zero(input * gain);
219 }
220
221 void apply_config(const Config& config) noexcept {
222 attack_coefficient_ =
224 release_coefficient_ =
226 }
227
228 std::array<SampleType, 2> process_detector(SampleType left, SampleType right) noexcept {
229 std::array<SampleType, 2> magnitudes{std::abs(left), std::abs(right)};
230 if (config_.stereo_link == DynamicsStereoLink::peak_linked) {
231 const SampleType shared = std::max(magnitudes[0], magnitudes[1]);
232 magnitudes = {shared, shared};
233 }
234
235 std::array<SampleType, 2> envelope{};
236 for (std::size_t channel = 0; channel < 2; ++channel) {
237 SampleType detector_value = magnitudes[channel];
238 if (config_.detector == DetectorMode::rms) {
239 const SampleType square_limit = std::sqrt(std::numeric_limits<SampleType>::max());
240 detector_value = magnitudes[channel] > square_limit
241 ? std::numeric_limits<SampleType>::max()
242 : magnitudes[channel] * magnitudes[channel];
243 }
244 const SampleType coefficient = detector_value > detector_state_[channel]
245 ? attack_coefficient_
246 : release_coefficient_;
247 detector_state_[channel] += coefficient * (detector_value - detector_state_[channel]);
248 if (!finite(detector_state_[channel]) || detector_state_[channel] < SampleType{0}) {
249 reset();
250 return {};
251 }
252 if (detector_state_[channel] < std::numeric_limits<SampleType>::min())
253 detector_state_[channel] = SampleType{0};
254 envelope[channel] = config_.detector == DetectorMode::rms
255 ? std::sqrt(detector_state_[channel])
256 : detector_state_[channel];
257 }
258 return envelope;
259 }
260
261 Config config_{};
262 std::array<SampleType, 2> detector_state_{};
263 std::array<SampleType, 2> current_gain_db_{};
264 SampleType attack_coefficient_ = SampleType{1};
265 SampleType release_coefficient_ = SampleType{1};
266 SampleType sample_rate_ = SampleType{0};
267 bool prepared_ = false;
268 bool bypassed_ = false;
269};
270
273
274} // namespace pulp::signal
static SampleType coefficient_for_time_ms(SampleType ms, SampleType sample_rate)
Definition dynamics_contract.hpp:80
typename Base::Mode Mode
Definition dynamics_contract.hpp:76
Stereo upward/downward expander with a continuous bounded gain law.
Definition expander.hpp:27
typename EnvelopeFollowerT< SampleType >::Mode DetectorMode
Definition expander.hpp:29
static constexpr float kMaxThresholdDb
Definition expander.hpp:44
static constexpr float kMaxRangeDb
Definition expander.hpp:47
void reset() noexcept
Definition expander.hpp:161
int latency_samples() const noexcept
Definition expander.hpp:175
bool bypassed() const noexcept
Definition expander.hpp:157
void set_bypassed(bool bypassed) noexcept
Definition expander.hpp:154
ExpanderStatus prepare(SampleType sample_rate) noexcept
Definition expander.hpp:51
static constexpr float kMaxKneeDb
Definition expander.hpp:48
ExpanderStatus configure(const Config &next) noexcept
Adopt a complete valid configuration.
Definition expander.hpp:63
static SampleType gain_computer_db(SampleType level_db, const Config &config) noexcept
Pure memoryless curve used by the processor after envelope detection.
Definition expander.hpp:78
std::array< GainReduction, 2 > gain_reduction() const noexcept
Definition expander.hpp:184
bool prepared() const noexcept
Definition expander.hpp:169
float sample_rate() const noexcept
Definition expander.hpp:172
std::array< SampleType, 2 > current_gain_db() const noexcept
Definition expander.hpp:181
const Config & config() const noexcept
Definition expander.hpp:166
static constexpr float kMaxRatio
Definition expander.hpp:46
static constexpr float kMinThresholdDb
Definition expander.hpp:43
void process(SampleType *left, SampleType *right, int num_samples) noexcept
Definition expander.hpp:144
std::array< SampleType, 2 > process(SampleType left, SampleType right) noexcept
Definition expander.hpp:118
static constexpr float kMaxSampleRate
Definition expander.hpp:49
int tail_samples() const noexcept
Definition expander.hpp:178
static constexpr float kMinRatio
Definition expander.hpp:45
static GainReduction from_signed_db(double signed_db) noexcept
Adapt a signed gain value, where attenuation is negative dB.
Definition dynamics_contract.hpp:32
Shared envelope and gain-reduction vocabulary for dynamics processors.
@ shared
Definition device.hpp:32
@ envelope
Definition plugin_state_io.hpp:26
Definition additive_bank.hpp:218
@ right
Definition multi_channel_meter.hpp:67
@ left
Definition multi_channel_meter.hpp:66
ExpanderT< double > Expander64
Definition expander.hpp:272
ExpanderT< float > Expander
Definition expander.hpp:271
ExpansionMode
Definition expander.hpp:16
@ upward
Definition expander.hpp:16
@ downward
Definition expander.hpp:16
@ invalid_config
Definition auto_ducked_send.hpp:18
@ invalid_sample_rate
Definition auto_ducked_send.hpp:18
@ ready
Definition auto_ducked_send.hpp:18
T snap_to_zero(T x)
Returns 0 if |x| < snap_threshold, else x.
Definition denormal.hpp:45
@ i
Definition formant_filter_bank.hpp:20
ExpanderStatus
Definition expander.hpp:17
@ invalid_config
Definition expander.hpp:17
@ invalid_sample_rate
Definition expander.hpp:17
@ ready
Definition expander.hpp:17
DynamicsStereoLink
Shared policy for dynamics processors with two detector channels.
Definition dynamics_contract.hpp:15
@ peak_linked
Definition dynamics_contract.hpp:15
@ independent
Definition dynamics_contract.hpp:15
Definition expander.hpp:31
SampleType attack_ms
Definition expander.hpp:37
ExpansionMode mode
Definition expander.hpp:32
SampleType threshold_db
Definition expander.hpp:33
SampleType release_ms
Definition expander.hpp:38
DynamicsStereoLink stereo_link
Definition expander.hpp:40
SampleType ratio
Definition expander.hpp:34
DetectorMode detector
Definition expander.hpp:39
SampleType knee_db
Definition expander.hpp:36
SampleType range_db
Definition expander.hpp:35