Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
dry_wet_mixer.hpp
Go to the documentation of this file.
1#pragma once
2
3// Dry/wet mix with optional latency compensation.
4// Use to blend original (dry) signal with processed (wet) signal.
5
6#include <vector>
7#include <cmath>
8#include <algorithm>
9#include <cstring>
10
11namespace pulp::signal {
12
39
40template <typename SampleType = float>
42public:
48 void set_mix(SampleType mix) {
49 mix_ = std::clamp(mix, SampleType{0.0f}, SampleType{1.0f});
50 if (ramp_samples_ <= 0) {
51 current_mix_ = mix_;
52 ramp_remaining_ = 0;
53 mix_increment_ = SampleType{0.0f};
54 return;
55 }
56 mix_increment_ = (mix_ - current_mix_) /
57 static_cast<SampleType>(ramp_samples_);
58 ramp_remaining_ = ramp_samples_;
59 }
60
62 SampleType mix() const { return mix_; }
63
66 SampleType current_mix() const { return current_mix_; }
67
84 void set_ramp_samples(int samples) {
85 const int new_ramp = std::max(0, samples);
86 if (new_ramp == ramp_samples_) return;
87 ramp_samples_ = new_ramp;
88 if (ramp_samples_ <= 0) {
89 // Dropping the ramp settles immediately rather than leaving a
90 // partially-applied ratio in place.
91 current_mix_ = mix_;
92 ramp_remaining_ = 0;
93 mix_increment_ = SampleType{0.0f};
94 return;
95 }
96 if (ramp_remaining_ > 0) {
97 ramp_remaining_ = std::min(ramp_remaining_, ramp_samples_);
98 mix_increment_ = ramp_remaining_ > 0
99 ? (mix_ - current_mix_) /
100 static_cast<SampleType>(ramp_remaining_)
101 : SampleType{0.0f};
102 }
103 }
104 int ramp_samples() const { return ramp_samples_; }
105
107 void set_ramp_time(SampleType seconds, SampleType sample_rate) {
108 if (!(seconds > SampleType{0.0f}) || !(sample_rate > SampleType{0.0f})) {
110 return;
111 }
112 set_ramp_samples(static_cast<int>(seconds * sample_rate));
113 }
114
116 bool is_ramping() const { return ramp_remaining_ > 0; }
117
119 void set_curve(MixCurve curve) { curve_ = curve; }
120
122 void set_wet_latency(int samples) {
123 const int new_latency = std::max(0, samples);
124 if (new_latency == latency_)
125 return;
126
127 latency_ = new_latency;
128 delay_pos_ = 0;
129
130 if (latency_ > 0 && max_channels_ > 0)
131 delay_buffer_.assign(static_cast<size_t>(latency_ * max_channels_),
132 SampleType{0.0f});
133 else
134 delay_buffer_.clear();
135
136 for (auto& ch : dry_buffer_)
137 std::fill(ch.begin(), ch.end(), SampleType{0.0f});
138 }
139
151 void prepare(int max_channels, int max_block_size) {
152 settle_ramp();
153 max_channels_ = std::max(0, max_channels);
154 max_block_size_ = std::max(0, max_block_size);
155 delay_pos_ = 0;
156 if (latency_ > 0 && max_channels_ > 0)
157 delay_buffer_.assign(static_cast<size_t>(latency_ * max_channels_),
158 SampleType{0.0f});
159 else
160 delay_buffer_.clear();
161
162 dry_buffer_.resize(static_cast<size_t>(max_channels_));
163 for (auto& ch : dry_buffer_)
164 ch.assign(static_cast<size_t>(max_block_size_), SampleType{0.0f});
165 active_dry_channels_ = 0;
166 active_dry_samples_ = 0;
167 }
168
170 void push_dry(const SampleType* const* channels, int num_channels, int num_samples) {
171 if (channels == nullptr || num_channels <= 0 || num_samples <= 0) {
172 active_dry_channels_ = 0;
173 active_dry_samples_ = 0;
174 return;
175 }
176
177 ensure_dry_storage(num_channels, num_samples);
178
179 if (latency_ <= 0) {
180 // No latency compensation — just store the dry signal
181 for (int ch = 0; ch < num_channels; ++ch) {
182 std::memcpy(dry_buffer_[ch].data(), channels[ch],
183 static_cast<size_t>(num_samples) * sizeof(SampleType));
184 }
185 active_dry_channels_ = num_channels;
186 active_dry_samples_ = num_samples;
187 return;
188 }
189
190 // With latency compensation — delay the dry signal.
191 // Process sample-by-sample (all channels per sample) so the delay
192 // position advances once per frame, not once per channel.
193 const int count = std::min(num_channels, max_channels_);
194
195 for (int i = 0; i < num_samples; ++i) {
196 for (int ch = 0; ch < count; ++ch) {
197 size_t idx = static_cast<size_t>(delay_pos_ * max_channels_ + ch);
198 dry_buffer_[ch][i] = delay_buffer_[idx];
199 delay_buffer_[idx] = channels[ch][i];
200 }
201 delay_pos_ = (delay_pos_ + 1) % latency_;
202 }
203 active_dry_channels_ = count;
204 active_dry_samples_ = num_samples;
205 }
206
208 void mix_wet(SampleType* const* wet_channels, int num_channels, int num_samples) {
209 const int channel_count = std::min(num_channels, active_dry_channels_);
210 const int sample_count = std::min(num_samples, active_dry_samples_);
211 if (channel_count <= 0 || sample_count <= 0) return;
212
213 if (ramp_remaining_ <= 0) {
214 // Settled: one gain pair for the whole block.
215 SampleType dry_gain, wet_gain;
216 compute_gains(current_mix_, dry_gain, wet_gain);
217 for (int ch = 0; ch < channel_count; ++ch) {
218 for (int i = 0; i < sample_count; ++i) {
219 wet_channels[ch][i] = dry_buffer_[ch][i] * dry_gain +
220 wet_channels[ch][i] * wet_gain;
221 }
222 }
223 return;
224 }
225
226 // Ramping: the ratio advances once per frame, so frames are the
227 // outer loop and every channel of a frame shares its gain pair.
228 for (int i = 0; i < sample_count; ++i) {
229 if (ramp_remaining_ > 0) {
230 current_mix_ += mix_increment_;
231 if (--ramp_remaining_ == 0) current_mix_ = mix_;
232 }
233 SampleType dry_gain, wet_gain;
234 compute_gains(current_mix_, dry_gain, wet_gain);
235 for (int ch = 0; ch < channel_count; ++ch) {
236 wet_channels[ch][i] = dry_buffer_[ch][i] * dry_gain +
237 wet_channels[ch][i] * wet_gain;
238 }
239 }
240 }
241
242 void reset() {
243 std::fill(delay_buffer_.begin(), delay_buffer_.end(), SampleType{0.0f});
244 delay_pos_ = 0;
245 for (auto& ch : dry_buffer_)
246 std::fill(ch.begin(), ch.end(), SampleType{0.0f});
247 settle_ramp();
248 }
249
250private:
251 SampleType mix_ = SampleType{1.0f};
252 SampleType current_mix_ = SampleType{1.0f};
253 SampleType mix_increment_ = SampleType{0.0f};
254 int ramp_samples_ = 0;
255 int ramp_remaining_ = 0;
256 MixCurve curve_ = MixCurve::Linear;
257 int latency_ = 0;
258 int max_channels_ = 2;
259 int max_block_size_ = 0;
260 int delay_pos_ = 0;
261 int active_dry_channels_ = 0;
262 int active_dry_samples_ = 0;
263 std::vector<SampleType> delay_buffer_;
264 std::vector<std::vector<SampleType>> dry_buffer_;
265
266 void ensure_dry_storage(int num_channels, int num_samples) {
267 if (num_channels <= static_cast<int>(dry_buffer_.size())) {
268 for (int ch = 0; ch < num_channels; ++ch) {
269 if (num_samples > static_cast<int>(dry_buffer_[ch].size()))
270 dry_buffer_[ch].resize(static_cast<size_t>(num_samples));
271 }
272 return;
273 }
274
275 const auto old_size = dry_buffer_.size();
276 dry_buffer_.resize(static_cast<size_t>(num_channels));
277 for (std::size_t ch = old_size; ch < dry_buffer_.size(); ++ch)
278 dry_buffer_[ch].resize(static_cast<size_t>(std::max(max_block_size_, num_samples)));
279 for (std::size_t ch = 0; ch < old_size; ++ch) {
280 if (num_samples > static_cast<int>(dry_buffer_[ch].size()))
281 dry_buffer_[ch].resize(static_cast<size_t>(num_samples));
282 }
283 }
284
285 void settle_ramp() {
286 current_mix_ = mix_;
287 ramp_remaining_ = 0;
288 mix_increment_ = SampleType{0.0f};
289 }
290
291 void compute_gains(SampleType ratio, SampleType& dry, SampleType& wet) const {
292 constexpr SampleType kHalfPi = SampleType{1.57079632679489661923f};
293 const SampleType theta = ratio * kHalfPi;
294 switch (curve_) {
295 case MixCurve::Linear:
296 dry = SampleType{1.0f} - ratio;
297 wet = ratio;
298 break;
300 case MixCurve::Sin3dB:
301 dry = std::cos(theta);
302 wet = std::sin(theta);
303 break;
305 dry = ratio <= SampleType{0.5f}
306 ? SampleType{1.0f}
307 : (SampleType{1.0f} - ratio) * SampleType{2.0f};
308 wet = ratio >= SampleType{0.5f}
309 ? SampleType{1.0f}
310 : ratio * SampleType{2.0f};
311 break;
312 case MixCurve::Sin4_5dB: {
313 // Clamp before pow to guard against tiny negative residue.
314 const SampleType c = std::max(SampleType{0.0f}, std::cos(theta));
315 const SampleType s = std::max(SampleType{0.0f}, std::sin(theta));
316 dry = std::pow(c, SampleType{1.5f});
317 wet = std::pow(s, SampleType{1.5f});
318 break;
319 }
320 case MixCurve::Sin6dB:
321 dry = std::cos(theta) * std::cos(theta);
322 wet = std::sin(theta) * std::sin(theta);
323 break;
325 dry = std::sqrt(SampleType{1.0f} - ratio);
326 wet = std::sqrt(ratio);
327 break;
329 // -4.5 dB = geometric mean of -3 dB sqrt (exp 0.5) and
330 // -6 dB linear (exp 1.0) → exponent 0.75. At mix=0.5
331 // produces 0.5^0.75 ≈ 0.5946 per side ≈ -4.51 dB,
332 // matching the documented midpoint notch.
333 dry = std::pow(SampleType{1.0f} - ratio, SampleType{0.75f});
334 wet = std::pow(ratio, SampleType{0.75f});
335 break;
336 }
337 }
338};
339
342
343} // namespace pulp::signal
Definition dry_wet_mixer.hpp:41
bool is_ramping() const
True while the ratio is still travelling toward its target.
Definition dry_wet_mixer.hpp:116
void push_dry(const SampleType *const *channels, int num_channels, int num_samples)
Push dry samples before processing (call before your wet processing).
Definition dry_wet_mixer.hpp:170
void reset()
Definition dry_wet_mixer.hpp:242
void set_ramp_time(SampleType seconds, SampleType sample_rate)
Convenience form of set_ramp_samples() in seconds.
Definition dry_wet_mixer.hpp:107
int ramp_samples() const
Definition dry_wet_mixer.hpp:104
void prepare(int max_channels, int max_block_size)
Prepare for processing.
Definition dry_wet_mixer.hpp:151
void set_wet_latency(int samples)
Set latency in samples for the wet path (compensates dry path delay).
Definition dry_wet_mixer.hpp:122
void set_mix(SampleType mix)
Set the mix ratio (0.0 = fully dry, 1.0 = fully wet).
Definition dry_wet_mixer.hpp:48
SampleType current_mix() const
The ratio the next sample will actually be mixed at.
Definition dry_wet_mixer.hpp:66
void set_curve(MixCurve curve)
Set the mixing curve type.
Definition dry_wet_mixer.hpp:119
float mix() const
Definition dry_wet_mixer.hpp:62
void set_ramp_samples(int samples)
Length of the crossfade applied when set_mix() changes the ratio.
Definition dry_wet_mixer.hpp:84
void mix_wet(SampleType *const *wet_channels, int num_channels, int num_samples)
Mix dry and wet signals, writing result to wet_channels (in-place).
Definition dry_wet_mixer.hpp:208
@ size
Definition config.hpp:216
Definition additive_bank.hpp:218
DryWetMixerT< float > DryWetMixer
Definition dry_wet_mixer.hpp:340
@ c
Definition spectrum_trace.hpp:35
@ ratio
base_frequency_hz * ratio.
Definition fm_operator_engine.hpp:20
@ EqualPower
(cos(u·π/2), sin(u·π/2)) — power sum == 1.
Definition crossfade.hpp:43
@ i
Definition formant_filter_bank.hpp:20
MixCurve
Crossfade curve between dry (mix=0) and wet (mix=1).
Definition dry_wet_mixer.hpp:29
@ Sqrt3dB
Definition dry_wet_mixer.hpp:36
@ Sqrt4_5dB
Definition dry_wet_mixer.hpp:37
@ Linear
Definition dry_wet_mixer.hpp:30
@ EqualPower
Definition dry_wet_mixer.hpp:31
@ Sin4_5dB
Definition dry_wet_mixer.hpp:34
@ Sin6dB
Definition dry_wet_mixer.hpp:35
@ Sin3dB
Definition dry_wet_mixer.hpp:33
@ Balanced
Definition dry_wet_mixer.hpp:32
DryWetMixerT< double > DryWetMixer64
Definition dry_wet_mixer.hpp:341
@ s
Definition input_events.hpp:41