Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
parallel_dynamics.hpp
Go to the documentation of this file.
1#pragma once
2
5
8#include <pulp/signal/detail/audio_range.hpp>
10#include <pulp/signal/units.hpp>
11
12#include <algorithm>
13#include <array>
14#include <atomic>
15#include <cmath>
16#include <cstddef>
17#include <cstdint>
18#include <limits>
19#include <memory>
20#include <new>
21#include <optional>
22#include <type_traits>
23#include <utility>
24
25namespace pulp::signal {
26
27template <typename SampleType = float>
29 SampleType wet_mix = SampleType{0.5};
31 SampleType output_gain_db = SampleType{0};
32 std::size_t dry_latency_samples = 0;
33 std::size_t wet_latency_samples = 0;
34 std::size_t dry_tail_samples = 0;
35 std::size_t wet_tail_samples = 0;
36};
37
38template <typename SampleType = float>
41 SampleType dry_gain = SampleType{0.5};
42 SampleType wet_gain = SampleType{0.5};
43 SampleType output_gain = SampleType{1};
44 std::size_t latency_samples = 0;
45 std::size_t tail_samples = 0;
46 std::uint64_t generation = 0;
47};
48
54template <typename SampleType = float>
55std::optional<PreparedParallelDynamicsConfigT<SampleType>>
57 std::size_t max_latency_samples) noexcept {
58 static_assert(std::is_floating_point_v<SampleType>);
59 static_assert(std::atomic<std::size_t>::is_always_lock_free,
60 "parallel dynamics reporting must be lock-free");
61 constexpr SampleType kMinOutputGainDb = SampleType{-120};
62 constexpr SampleType kMaxOutputGainDb = SampleType{24};
63
64 if (!std::isfinite(config.wet_mix) || config.wet_mix < SampleType{0} ||
65 config.wet_mix > SampleType{1} || !std::isfinite(config.output_gain_db) ||
66 config.output_gain_db < kMinOutputGainDb ||
67 config.output_gain_db > kMaxOutputGainDb ||
68 config.dry_latency_samples > max_latency_samples ||
69 config.wet_latency_samples > max_latency_samples ||
70 (config.mix_law != CrossfadeGainLaw::EqualGain &&
71 config.mix_law != CrossfadeGainLaw::EqualPower))
72 return std::nullopt;
73
75 result.config = config;
76 if (config.wet_mix == SampleType{0}) {
77 result.dry_gain = SampleType{1};
78 result.wet_gain = SampleType{0};
79 } else if (config.wet_mix == SampleType{1}) {
80 result.dry_gain = SampleType{0};
81 result.wet_gain = SampleType{1};
82 } else {
83 crossfade_gains(config.wet_mix, config.mix_law,
84 result.dry_gain, result.wet_gain);
85 }
86 result.output_gain = units::db_to_linear(config.output_gain_db);
87 if (!std::isfinite(result.output_gain)) return std::nullopt;
88
89 result.latency_samples =
90 std::max(config.dry_latency_samples, config.wet_latency_samples);
91 const auto dry_compensation =
92 result.latency_samples - config.dry_latency_samples;
93 const auto wet_compensation =
94 result.latency_samples - config.wet_latency_samples;
95 if (config.dry_tail_samples >
96 std::numeric_limits<std::size_t>::max() - dry_compensation ||
97 config.wet_tail_samples >
98 std::numeric_limits<std::size_t>::max() - wet_compensation)
99 return std::nullopt;
100 const auto aligned_dry_tail = config.dry_tail_samples + dry_compensation;
101 const auto aligned_wet_tail = config.wet_tail_samples + wet_compensation;
102 if (config.wet_mix == SampleType{0})
103 result.tail_samples = aligned_dry_tail;
104 else if (config.wet_mix == SampleType{1})
105 result.tail_samples = aligned_wet_tail;
106 else
107 result.tail_samples = std::max(aligned_dry_tail, aligned_wet_tail);
108 return result;
109}
110
123template <typename SampleType = float>
125public:
128
129 static_assert(std::is_floating_point_v<SampleType>);
130 ParallelDynamicsMixerT() noexcept : published_(default_config()) {}
131
132 bool prepare(std::size_t max_latency_samples, std::size_t max_block_size) {
133 if (max_block_size == 0 ||
134 max_block_size > std::numeric_limits<std::size_t>::max() / kStreamCount)
135 return false;
136
137 const auto scratch_samples = max_block_size * kStreamCount;
138 auto replacement_scratch =
139 std::unique_ptr<SampleType[]>(new (std::nothrow) SampleType[scratch_samples]);
140 if (!replacement_scratch) return false;
141 std::fill_n(replacement_scratch.get(), scratch_samples, SampleType{0});
142
143 PathLatencyAlignerT<SampleType, 2, 2> replacement_aligner;
144 if (!replacement_aligner.prepare(2, 2, max_latency_samples, max_block_size))
145 return false;
146
147 scratch_ = std::move(replacement_scratch);
148 aligner_ = std::move(replacement_aligner);
149 max_latency_samples_ = max_latency_samples;
150 max_block_size_ = max_block_size;
151 configured_dry_latency_ = 0;
152 configured_wet_latency_ = 0;
153 reported_latency_samples_.store(0, std::memory_order_relaxed);
154 reported_tail_samples_.store(0, std::memory_order_relaxed);
155 prepared_ = true;
156 active_ = {};
157
158 auto initial = default_config();
159 initial.generation = next_generation();
160 published_.write(initial);
161 return true;
162 }
163
164 bool publish_config(const Config& config) noexcept {
165 if (!prepared_) return false;
166 auto prepared =
167 prepare_parallel_dynamics_config(config, max_latency_samples_);
168 if (!prepared || config.dry_latency_samples != configured_dry_latency_ ||
169 config.wet_latency_samples != configured_wet_latency_)
170 return false;
171 prepared->generation = next_generation();
172 published_.write(*prepared);
173 return true;
174 }
175
177 bool configure_latency_and_publish(const Config& config) noexcept {
178 if (!prepared_) return false;
179 auto prepared =
180 prepare_parallel_dynamics_config(config, max_latency_samples_);
181 if (!prepared) return false;
182 const std::array<std::size_t, 2> latencies{
183 config.dry_latency_samples, config.wet_latency_samples};
184 if (!aligner_.configure_latencies(latencies)) return false;
185 configured_dry_latency_ = config.dry_latency_samples;
186 configured_wet_latency_ = config.wet_latency_samples;
187 prepared->generation = next_generation();
188 published_.write(*prepared);
189 active_ = *prepared;
190 publish_reporting(*prepared);
191 return true;
192 }
193
194 bool process(const SampleType* const* dry,
195 const SampleType* const* wet,
196 SampleType* const* output,
197 std::size_t frames) noexcept {
198 if (!validate_layout(dry, wet, output, frames)) return false;
199 if (!sync_config()) return false;
200 if (frames == 0) return true;
201
202 std::array<const SampleType*, kStreamCount> inputs{
203 dry[0], dry[1], wet[0], wet[1]};
204 std::array<SampleType*, kStreamCount> aligned{};
205 for (std::size_t stream = 0; stream < kStreamCount; ++stream)
206 aligned[stream] = scratch_.get() + stream * max_block_size_;
207
208 if (!aligner_.process(inputs.data(), inputs.size(), aligned.data(),
209 aligned.size(), frames))
210 return false;
211
212 for (std::size_t channel = 0; channel < kChannelCount; ++channel) {
213 const auto* aligned_dry = aligned[channel];
214 const auto* aligned_wet = aligned[kChannelCount + channel];
215 for (std::size_t frame = 0; frame < frames; ++frame) {
216 const auto dry_sample = finite_or_zero(aligned_dry[frame]);
217 const auto wet_sample = finite_or_zero(aligned_wet[frame]);
218 const auto mixed = (dry_sample * active_.dry_gain +
219 wet_sample * active_.wet_gain) *
220 active_.output_gain;
221 output[channel][frame] = finite_or_zero(mixed);
222 }
223 }
224 return true;
225 }
226
227 void reset() noexcept {
228 if (!prepared_) return;
229 if (!sync_config()) return;
230 aligner_.reset();
231 std::fill_n(scratch_.get(), max_block_size_ * kStreamCount, SampleType{0});
232 }
233
234 std::size_t latency_samples() const noexcept {
235 return reported_latency_samples_.load(std::memory_order_acquire);
236 }
237 std::size_t tail_samples() const noexcept {
238 return reported_tail_samples_.load(std::memory_order_acquire);
239 }
240 std::size_t max_block_size() const noexcept { return max_block_size_; }
241 bool prepared() const noexcept { return prepared_; }
242
243private:
244 static constexpr std::size_t kChannelCount = 2;
245 static constexpr std::size_t kStreamCount = 4;
246
247 static PreparedConfig default_config() noexcept {
249 }
250
251 std::uint64_t next_generation() noexcept {
252 ++writer_generation_;
253 if (writer_generation_ == 0) ++writer_generation_;
254 return writer_generation_;
255 }
256
257 static SampleType finite_or_zero(SampleType sample) noexcept {
258 return std::isfinite(sample) ? sample : SampleType{0};
259 }
260
261 bool sync_config() noexcept {
262 const auto& latest = published_.read();
263 if (latest.generation == active_.generation) return true;
264 active_ = latest;
265 publish_reporting(active_);
266 return true;
267 }
268
269 void publish_reporting(const PreparedConfig& config) noexcept {
270 reported_latency_samples_.store(config.latency_samples,
271 std::memory_order_release);
272 reported_tail_samples_.store(config.tail_samples,
273 std::memory_order_release);
274 }
275
276 bool validate_layout(const SampleType* const* dry,
277 const SampleType* const* wet,
278 SampleType* const* output,
279 std::size_t frames) const noexcept {
280 if (!prepared_ || frames > max_block_size_ || dry == nullptr ||
281 wet == nullptr || output == nullptr)
282 return false;
283 for (std::size_t channel = 0; channel < kChannelCount; ++channel)
284 if (dry[channel] == nullptr || wet[channel] == nullptr ||
285 output[channel] == nullptr)
286 return false;
287
288 if (frames == 0) return true;
289 const std::array<const SampleType*, 3> left{dry[0], wet[0], output[0]};
290 const std::array<const SampleType*, 3> right{dry[1], wet[1], output[1]};
291 for (const auto* left_range : left)
292 for (const auto* right_range : right)
293 if (detail::audio_ranges_overlap(left_range, right_range, frames))
294 return false;
295
296 for (std::size_t channel = 0; channel < kChannelCount; ++channel) {
297 if (dry[channel] != wet[channel] &&
298 detail::audio_ranges_overlap(dry[channel], wet[channel], frames))
299 return false;
300 if (output[channel] != dry[channel] &&
301 detail::audio_ranges_overlap(output[channel], dry[channel], frames))
302 return false;
303 if (output[channel] != wet[channel] &&
304 detail::audio_ranges_overlap(output[channel], wet[channel], frames))
305 return false;
306 }
307 return true;
308 }
309
310 PathLatencyAlignerT<SampleType, 2, 2> aligner_;
311 std::unique_ptr<SampleType[]> scratch_;
312 pulp::runtime::TripleBuffer<PreparedConfig> published_;
313 PreparedConfig active_{};
314 std::size_t max_latency_samples_ = 0;
315 std::size_t max_block_size_ = 0;
316 std::size_t configured_dry_latency_ = 0;
317 std::size_t configured_wet_latency_ = 0;
318 std::atomic<std::size_t> reported_latency_samples_{0};
319 std::atomic<std::size_t> reported_tail_samples_{0};
320 std::uint64_t writer_generation_ = 0;
321 bool prepared_ = false;
322};
323
330
331} // namespace pulp::signal
Blend true-stereo dry input with an already-processed true-stereo wet path.
Definition parallel_dynamics.hpp:124
ParallelDynamicsConfigT< SampleType > Config
Definition parallel_dynamics.hpp:126
ParallelDynamicsMixerT() noexcept
Definition parallel_dynamics.hpp:130
void reset() noexcept
Definition parallel_dynamics.hpp:227
bool prepared() const noexcept
Definition parallel_dynamics.hpp:241
bool process(const SampleType *const *dry, const SampleType *const *wet, SampleType *const *output, std::size_t frames) noexcept
Definition parallel_dynamics.hpp:194
bool publish_config(const Config &config) noexcept
Definition parallel_dynamics.hpp:164
std::size_t tail_samples() const noexcept
Definition parallel_dynamics.hpp:237
PreparedParallelDynamicsConfigT< SampleType > PreparedConfig
Definition parallel_dynamics.hpp:127
bool configure_latency_and_publish(const Config &config) noexcept
Change alignment and publish a validated snapshot while audio is stopped.
Definition parallel_dynamics.hpp:177
std::size_t latency_samples() const noexcept
Definition parallel_dynamics.hpp:234
std::size_t max_block_size() const noexcept
Definition parallel_dynamics.hpp:240
bool prepare(std::size_t max_latency_samples, std::size_t max_block_size)
Definition parallel_dynamics.hpp:132
Aligns N already-processed paths to the largest declared intrinsic latency.
Definition path_latency_aligner.hpp:28
bool prepare(std::size_t paths, std::size_t channels, std::size_t max_latency_samples, std::size_t max_block_size)
Definition path_latency_aligner.hpp:37
The one shared old→new crossfade gain law for the SIGNAL side.
@ config
Per-user settings root (an XDG distinction; equals app_data off Linux).
Definition special_dir.hpp:65
T db_to_linear(T db)
Amplitude dB → linear gain. 0 dB → 1, −6.0206 dB → 0.5.
Definition units.hpp:71
Definition additive_bank.hpp:218
@ right
Definition multi_channel_meter.hpp:67
@ left
Definition multi_channel_meter.hpp:66
ParallelDynamicsConfigT< double > ParallelDynamicsConfig64
Definition parallel_dynamics.hpp:325
ParallelDynamicsConfigT< float > ParallelDynamicsConfig
Definition parallel_dynamics.hpp:324
PreparedParallelDynamicsConfigT< float > PreparedParallelDynamicsConfig
Definition parallel_dynamics.hpp:326
ParallelDynamicsMixerT< float > ParallelDynamicsMixer
Definition parallel_dynamics.hpp:328
CrossfadeGainLaw
The old→new gain split applied to a shaped progress u ∈ [0,1].
Definition crossfade.hpp:41
@ EqualPower
(cos(u·π/2), sin(u·π/2)) — power sum == 1.
Definition crossfade.hpp:43
@ EqualGain
(1−u, u) — amplitude sum == 1.
Definition crossfade.hpp:42
void crossfade_gains(T u, CrossfadeGainLaw law, T &old_gain, T &new_gain) noexcept
Old/new blend gains for an ALREADY-SHAPED progress u (the caller applies any smoothstep/clamp first).
Definition crossfade.hpp:61
std::optional< PreparedParallelDynamicsConfigT< SampleType > > prepare_parallel_dynamics_config(const ParallelDynamicsConfigT< SampleType > &config, std::size_t max_latency_samples) noexcept
Validate a complete control snapshot and derive its audio-rate gains.
Definition parallel_dynamics.hpp:56
ParallelDynamicsMixerT< double > ParallelDynamicsMixer64
Definition parallel_dynamics.hpp:329
PreparedParallelDynamicsConfigT< double > PreparedParallelDynamicsConfig64
Definition parallel_dynamics.hpp:327
Definition parallel_dynamics.hpp:28
std::size_t wet_tail_samples
Definition parallel_dynamics.hpp:35
std::size_t wet_latency_samples
Definition parallel_dynamics.hpp:33
SampleType wet_mix
Definition parallel_dynamics.hpp:29
SampleType output_gain_db
Definition parallel_dynamics.hpp:31
std::size_t dry_tail_samples
Definition parallel_dynamics.hpp:34
CrossfadeGainLaw mix_law
Definition parallel_dynamics.hpp:30
std::size_t dry_latency_samples
Definition parallel_dynamics.hpp:32
Definition parallel_dynamics.hpp:39
ParallelDynamicsConfigT< SampleType > config
Definition parallel_dynamics.hpp:40
SampleType wet_gain
Definition parallel_dynamics.hpp:42
SampleType dry_gain
Definition parallel_dynamics.hpp:41
SampleType output_gain
Definition parallel_dynamics.hpp:43
std::size_t latency_samples
Definition parallel_dynamics.hpp:44
std::uint64_t generation
Definition parallel_dynamics.hpp:46
std::size_t tail_samples
Definition parallel_dynamics.hpp:45
Lock-free latest-value publication between two threads.
The real-unit vocabulary shared by every DSP block in the catalog.