Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
spectral_cross_synthesis.hpp
Go to the documentation of this file.
1#pragma once
2
10
14
15#include <algorithm>
16#include <array>
17#include <cmath>
18#include <complex>
19#include <cstddef>
20#include <cstdint>
21#include <limits>
22#include <new>
23#include <span>
24#include <stdexcept>
25#include <type_traits>
26#include <utility>
27#include <vector>
28
29namespace pulp::signal {
30
35
36template <typename SampleType> struct SpectralCrossSynthesisPrepareConfigT {
37 int channels = 1;
38 int fft_size = 2048;
40 int lifter_order = -1;
42 SampleType convergence_tolerance = SampleType{0};
43};
44
45template <typename SampleType> struct SpectralCrossSynthesisConfigT {
47 SampleType amount = SampleType{1};
49 SampleType mix = SampleType{1};
53 SampleType maximum_transfer_db = SampleType{60};
55 SampleType magnitude_floor = SampleType{1e-12};
58};
59
60template <typename SampleType = float> class SpectralCrossSynthesisT {
61 public:
62 static_assert(std::is_floating_point_v<SampleType>);
63
66
67 static constexpr int maximum_channels = 64;
68 static constexpr int maximum_true_envelope_iterations = 32;
69 static constexpr int maximum_smoothing_frames = 4096;
70 static constexpr SampleType maximum_transfer_db = SampleType{120};
71
72 static bool supports_configuration(const PrepareConfig& config) noexcept {
73 if (config.channels <= 0 || config.channels > maximum_channels ||
76 (config.fft_size & (config.fft_size - 1)) != 0 || config.lifter_order < -1 ||
77 config.lifter_order > config.fft_size / 2 || config.true_envelope_iterations < 0 ||
78 config.true_envelope_iterations > maximum_true_envelope_iterations ||
79 !std::isfinite(config.convergence_tolerance) ||
80 config.convergence_tolerance < SampleType{0})
81 return false;
82 return true;
83 }
84
89 return false;
90 if (config.lifter_order < 0)
91 config.lifter_order = config.fft_size / 16;
92
93 typename CepstralEnvelopeAnalyzerT<SampleType>::Config analyzer_config;
94 analyzer_config.fft_size = config.fft_size;
95 analyzer_config.order = config.lifter_order;
96 analyzer_config.true_envelope_iterations = config.true_envelope_iterations;
97 analyzer_config.convergence_tolerance = config.convergence_tolerance;
98
99 std::uint64_t analyzer_bytes = 0;
101 analyzer_config, kTargetAddressMaximumBytes, analyzer_bytes))
102 return false;
103 const auto bins = static_cast<std::uint64_t>(config.fft_size / 2 + 1);
104 std::uint64_t elements = 0;
105 if (!checked_capacity_product(static_cast<std::uint64_t>(config.channels), bins,
106 std::numeric_limits<std::uint64_t>::max(), elements))
107 return false;
109 if (!charge.add_retained_bytes(analyzer_bytes) || !charge.add<SampleType>(elements * 8u) ||
110 !charge.add<std::uint8_t>(static_cast<std::uint64_t>(config.channels)))
111 return false;
112
113#if defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)
114 try {
115#else
116 {
117#endif
119 if (next_analyzer.prepare(analyzer_config) != SourceFilterAnalysisStatus::Ok)
120 return false;
121 const auto count = static_cast<std::size_t>(elements);
122 std::vector<SampleType> next_carrier_log(count, SampleType{0});
123 std::vector<SampleType> next_modulator_log(count, SampleType{0});
124 std::vector<SampleType> next_carrier_envelope(count, SampleType{0});
125 std::vector<SampleType> next_modulator_envelope(count, SampleType{0});
126 std::vector<SampleType> next_carrier_history(count, SampleType{0});
127 std::vector<SampleType> next_modulator_history(count, SampleType{0});
128 std::vector<SampleType> next_carrier_magnitude(count, SampleType{0});
129 std::vector<SampleType> next_wet_magnitude(count, SampleType{0});
130 std::vector<std::uint8_t> next_history_valid(static_cast<std::size_t>(config.channels),
131 std::uint8_t{0});
132
133 analyzer_ = std::move(next_analyzer);
134 carrier_log_ = std::move(next_carrier_log);
135 modulator_log_ = std::move(next_modulator_log);
136 carrier_envelope_ = std::move(next_carrier_envelope);
137 modulator_envelope_ = std::move(next_modulator_envelope);
138 carrier_history_ = std::move(next_carrier_history);
139 modulator_history_ = std::move(next_modulator_history);
140 carrier_magnitude_ = std::move(next_carrier_magnitude);
141 wet_magnitude_ = std::move(next_wet_magnitude);
142 history_valid_ = std::move(next_history_valid);
143 prepare_config_ = config;
144 num_bins_ = config.fft_size / 2 + 1;
145 retained_bytes_ = charge.total();
146 return true;
147#if defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)
148 } catch (const std::bad_alloc&) {
149 return false;
150 } catch (const std::length_error&) {
151 return false;
152 }
153#else
154 }
155#endif
156 }
157
160 bool set_config(const Config& config) noexcept {
161 if (!valid_config(config))
162 return false;
163 const SampleType next_retain =
164 config.envelope_smoothing_frames <= 1
165 ? SampleType{0}
166 : std::exp(-SampleType{1} /
167 static_cast<SampleType>(config.envelope_smoothing_frames));
168 config_ = config;
169 envelope_retain_ = next_retain;
170 return true;
171 }
172
173 Config config() const noexcept {
174 return config_;
175 }
176 bool prepared() const noexcept {
177 return num_bins_ > 0;
178 }
179 int channels() const noexcept {
180 return prepared() ? prepare_config_.channels : 0;
181 }
182 int fft_size() const noexcept {
183 return prepared() ? prepare_config_.fft_size : 0;
184 }
185 int num_bins() const noexcept {
186 return num_bins_;
187 }
188 int lifter_order() const noexcept {
189 return prepared() ? prepare_config_.lifter_order : 0;
190 }
191 std::uint64_t retained_bytes() const noexcept {
192 return retained_bytes_;
193 }
194
197 void reset() noexcept {
198 std::fill(carrier_history_.begin(), carrier_history_.end(), SampleType{0});
199 std::fill(modulator_history_.begin(), modulator_history_.end(), SampleType{0});
200 std::fill(history_valid_.begin(), history_valid_.end(), std::uint8_t{0});
201 }
202
207 bool process(const std::complex<SampleType>* const* carrier,
208 const std::complex<SampleType>* const* modulator,
209 std::complex<SampleType>* const* output, int channels, int num_bins) noexcept {
210 if (!prepared() || channels != prepare_config_.channels || num_bins != num_bins_ ||
211 carrier == nullptr || modulator == nullptr || output == nullptr)
212 return false;
213 for (int ch = 0; ch < channels; ++ch) {
214 if (carrier[ch] == nullptr || modulator[ch] == nullptr || output[ch] == nullptr)
215 return false;
216 }
217
218 for (int ch = 0; ch < channels; ++ch) {
219 const auto offset = static_cast<std::size_t>(ch) * static_cast<std::size_t>(num_bins_);
220 for (int bin = 0; bin < num_bins_; ++bin) {
221 const auto index = offset + static_cast<std::size_t>(bin);
222 const bool endpoint = bin == 0 || bin == num_bins_ - 1;
223 const SampleType carrier_magnitude = decoded_magnitude(carrier[ch][bin], endpoint);
224 const SampleType modulator_magnitude =
225 decoded_magnitude(modulator[ch][bin], endpoint);
226 carrier_magnitude_[index] = carrier_magnitude;
227 carrier_log_[index] =
228 std::log(std::max(carrier_magnitude, config_.magnitude_floor));
229 modulator_log_[index] =
230 std::log(std::max(modulator_magnitude, config_.magnitude_floor));
231 }
232 const auto carrier_result =
233 analyzer_.estimate(std::span<const SampleType>{carrier_log_.data() + offset,
234 static_cast<std::size_t>(num_bins_)},
235 std::span<SampleType>{carrier_envelope_.data() + offset,
236 static_cast<std::size_t>(num_bins_)});
237 if (!carrier_result.ok())
238 return false;
239 const auto modulator_result =
240 analyzer_.estimate(std::span<const SampleType>{modulator_log_.data() + offset,
241 static_cast<std::size_t>(num_bins_)},
242 std::span<SampleType>{modulator_envelope_.data() + offset,
243 static_cast<std::size_t>(num_bins_)});
244 if (!modulator_result.ok())
245 return false;
246 }
247
248 const SampleType maximum_transfer_ln =
249 config_.maximum_transfer_db * static_cast<SampleType>(0.1151292546497022842L);
250 for (int ch = 0; ch < channels; ++ch) {
251 const auto offset = static_cast<std::size_t>(ch) * static_cast<std::size_t>(num_bins_);
252 const bool initialize = history_valid_[static_cast<std::size_t>(ch)] == 0;
253 for (int bin = 0; bin < num_bins_; ++bin) {
254 const auto index = offset + static_cast<std::size_t>(bin);
255 if (initialize) {
256 carrier_history_[index] = carrier_envelope_[index];
257 modulator_history_[index] = modulator_envelope_[index];
258 } else {
259 carrier_history_[index] = std::fma(
260 envelope_retain_, carrier_history_[index] - carrier_envelope_[index],
261 carrier_envelope_[index]);
262 modulator_history_[index] = std::fma(
263 envelope_retain_, modulator_history_[index] - modulator_envelope_[index],
264 modulator_envelope_[index]);
265 }
266 const SampleType transfer = std::clamp(
267 config_.amount * (modulator_history_[index] - carrier_history_[index]),
268 -maximum_transfer_ln, maximum_transfer_ln);
269 wet_magnitude_[index] = transferred_magnitude(carrier_magnitude_[index], transfer);
270 }
271 history_valid_[static_cast<std::size_t>(ch)] = 1;
272
273 SampleType normalization_gain = SampleType{1};
275 normalization_gain = rms_ratio(carrier_magnitude_.data() + offset,
276 wet_magnitude_.data() + offset, num_bins_);
277
278 for (int bin = 0; bin < num_bins_; ++bin) {
279 const auto index = offset + static_cast<std::size_t>(bin);
280 const auto carrier_value = carrier[ch][bin];
281 if (!finite(carrier_value) || carrier_magnitude_[index] == SampleType{0}) {
282 output[ch][bin] = {};
283 continue;
284 }
285 const SampleType magnitude = mixed_magnitude(
286 carrier_magnitude_[index], wet_magnitude_[index], normalization_gain,
287 config_.mix);
288 const bool endpoint = bin == 0 || bin == num_bins_ - 1;
289 if (endpoint) {
290 const SampleType sign =
291 carrier_value.real() < SampleType{0} ? SampleType{-1} : SampleType{1};
292 output[ch][bin] = {sign * magnitude, SampleType{0}};
293 } else {
294 output[ch][bin] = unit_phase(carrier_value) * magnitude;
295 }
296 }
297 }
298 return true;
299 }
300
303 static constexpr int added_latency_samples() noexcept {
304 return 0;
305 }
306 static constexpr int added_tail_samples() noexcept {
307 return 0;
308 }
309
310 private:
311 static bool valid_config(const Config& config) noexcept {
312 const bool normalization_valid =
315 return std::isfinite(config.amount) && config.amount >= SampleType{0} &&
316 config.amount <= SampleType{1} && std::isfinite(config.mix) &&
317 config.mix >= SampleType{0} && config.mix <= SampleType{1} &&
318 config.envelope_smoothing_frames >= 1 &&
319 config.envelope_smoothing_frames <= maximum_smoothing_frames &&
320 std::isfinite(config.maximum_transfer_db) &&
321 config.maximum_transfer_db >= SampleType{0} &&
322 config.maximum_transfer_db <= maximum_transfer_db &&
323 std::isfinite(config.magnitude_floor) && config.magnitude_floor > SampleType{0} &&
324 normalization_valid;
325 }
326
327 static bool finite(std::complex<SampleType> value) noexcept {
328 return std::isfinite(value.real()) && std::isfinite(value.imag());
329 }
330
331 static SampleType decoded_magnitude(std::complex<SampleType> value, bool endpoint) noexcept {
332 if (!finite(value))
333 return SampleType{0};
334 if (endpoint)
335 return std::abs(value.real());
336 const SampleType scale = std::max(std::abs(value.real()), std::abs(value.imag()));
337 if (scale == SampleType{0})
338 return SampleType{0};
339 const SampleType normalized = std::hypot(value.real() / scale, value.imag() / scale);
340 const SampleType maximum = std::numeric_limits<SampleType>::max();
341 return scale > maximum / normalized ? maximum : scale * normalized;
342 }
343
344 static std::complex<SampleType> unit_phase(std::complex<SampleType> value) noexcept {
345 const SampleType scale = std::max(std::abs(value.real()), std::abs(value.imag()));
346 const SampleType real = value.real() / scale;
347 const SampleType imag = value.imag() / scale;
348 const SampleType length = std::hypot(real, imag);
349 return {real / length, imag / length};
350 }
351
352 static SampleType saturate_positive(long double value) noexcept {
353 if (!(value > 0.0L))
354 return SampleType{0};
355 const long double maximum =
356 static_cast<long double>(std::numeric_limits<SampleType>::max());
357 return static_cast<SampleType>(std::min(value, maximum));
358 }
359
360 static SampleType saturating_positive_product(SampleType a, SampleType b,
361 SampleType c) noexcept {
362 std::array<SampleType, 3> factors{a, b, c};
363 std::sort(factors.begin(), factors.end());
364 if (!(factors[0] > SampleType{0}))
365 return SampleType{0};
366 const SampleType maximum = std::numeric_limits<SampleType>::max();
367 if (factors[0] > maximum / factors[1])
368 return maximum;
369 const SampleType partial = factors[0] * factors[1];
370 return partial > maximum / factors[2] ? maximum : partial * factors[2];
371 }
372
373 static SampleType mixed_magnitude(SampleType dry, SampleType wet,
374 SampleType normalization_gain, SampleType mix) noexcept {
375 if (mix == SampleType{0})
376 return dry;
377 if (mix == SampleType{1})
378 return saturating_positive_product(wet, normalization_gain, SampleType{1});
379 const SampleType dry_component = (SampleType{1} - mix) * dry;
380 const SampleType wet_component = saturating_positive_product(wet, normalization_gain, mix);
381 const SampleType maximum = std::numeric_limits<SampleType>::max();
382 return dry_component > maximum - wet_component ? maximum
383 : dry_component + wet_component;
384 }
385
386 static SampleType transferred_magnitude(SampleType carrier_magnitude,
387 SampleType transfer) noexcept {
388 if (carrier_magnitude == SampleType{0})
389 return SampleType{0};
390 const long double log_target = std::log(static_cast<long double>(carrier_magnitude)) +
391 static_cast<long double>(transfer);
392 const long double maximum_log =
393 std::log(static_cast<long double>(std::numeric_limits<SampleType>::max()));
394 return saturate_positive(std::exp(std::min(log_target, maximum_log)));
395 }
396
397 static SampleType rms_ratio(const SampleType* dry, const SampleType* wet, int count) noexcept {
398 long double dry_scale = 0.0L;
399 long double dry_sum = 0.0L;
400 long double wet_scale = 0.0L;
401 long double wet_sum = 0.0L;
402 const auto add_scaled_square = [](SampleType value, long double& scale,
403 long double& sum) noexcept {
404 const long double magnitude = std::abs(static_cast<long double>(value));
405 if (magnitude == 0.0L)
406 return;
407 if (scale < magnitude) {
408 const long double ratio = scale / magnitude;
409 sum = 1.0L + sum * ratio * ratio;
410 scale = magnitude;
411 } else {
412 const long double ratio = magnitude / scale;
413 sum += ratio * ratio;
414 }
415 };
416 for (int index = 0; index < count; ++index) {
417 add_scaled_square(dry[index], dry_scale, dry_sum);
418 add_scaled_square(wet[index], wet_scale, wet_sum);
419 }
420 if (!(dry_scale > 0.0L) || !(wet_scale > 0.0L))
421 return SampleType{1};
422 const long double ratio = (dry_scale / wet_scale) * std::sqrt(dry_sum / wet_sum);
423 return saturate_positive(ratio);
424 }
425
426 PrepareConfig prepare_config_{};
427 Config config_{};
428 CepstralEnvelopeAnalyzerT<SampleType> analyzer_{};
429 int num_bins_ = 0;
430 SampleType envelope_retain_ = SampleType{0};
431 std::uint64_t retained_bytes_ = 0;
432 std::vector<SampleType> carrier_log_;
433 std::vector<SampleType> modulator_log_;
434 std::vector<SampleType> carrier_envelope_;
435 std::vector<SampleType> modulator_envelope_;
436 std::vector<SampleType> carrier_history_;
437 std::vector<SampleType> modulator_history_;
438 std::vector<SampleType> carrier_magnitude_;
439 std::vector<SampleType> wet_magnitude_;
440 std::vector<std::uint8_t> history_valid_;
441};
442
449
450} // namespace pulp::signal
Log-magnitude spectrum to liftered log-envelope.
Definition source_filter_analysis.hpp:83
SourceFilterAnalysisStatus prepare(const Config &config)
Definition source_filter_analysis.hpp:107
static bool checked_retained_bytes(const Config &config, std::uint64_t target_max_bytes, std::uint64_t &bytes) noexcept
Definition source_filter_analysis.hpp:90
CepstralEnvelopeConfigT< SampleType > Config
Definition source_filter_analysis.hpp:87
Definition checked_allocation.hpp:57
bool add(std::uint64_t elements) noexcept
Definition checked_allocation.hpp:63
std::uint64_t total() const noexcept
Definition checked_allocation.hpp:83
bool add_retained_bytes(std::uint64_t bytes) noexcept
Definition checked_allocation.hpp:78
Definition spectral_cross_synthesis.hpp:60
static constexpr int maximum_channels
Definition spectral_cross_synthesis.hpp:67
SpectralCrossSynthesisPrepareConfigT< SampleType > PrepareConfig
Definition spectral_cross_synthesis.hpp:64
bool prepared() const noexcept
Definition spectral_cross_synthesis.hpp:176
static constexpr int maximum_true_envelope_iterations
Definition spectral_cross_synthesis.hpp:68
void reset() noexcept
Clear temporal envelope history while preserving prepared capacity and both configs.
Definition spectral_cross_synthesis.hpp:197
int channels() const noexcept
Definition spectral_cross_synthesis.hpp:179
bool prepare(PrepareConfig config)
Allocate the analyzer, envelope history, and all frame scratch off the audio thread.
Definition spectral_cross_synthesis.hpp:87
Config config() const noexcept
Definition spectral_cross_synthesis.hpp:173
static constexpr int added_tail_samples() noexcept
Definition spectral_cross_synthesis.hpp:306
SpectralCrossSynthesisConfigT< SampleType > Config
Definition spectral_cross_synthesis.hpp:65
static constexpr int maximum_smoothing_frames
Definition spectral_cross_synthesis.hpp:69
bool set_config(const Config &config) noexcept
Replace every runtime control transactionally.
Definition spectral_cross_synthesis.hpp:160
static bool supports_configuration(const PrepareConfig &config) noexcept
Definition spectral_cross_synthesis.hpp:72
int lifter_order() const noexcept
Definition spectral_cross_synthesis.hpp:188
int num_bins() const noexcept
Definition spectral_cross_synthesis.hpp:185
std::uint64_t retained_bytes() const noexcept
Definition spectral_cross_synthesis.hpp:191
int fft_size() const noexcept
Definition spectral_cross_synthesis.hpp:182
static constexpr int added_latency_samples() noexcept
Analysis/synthesis buffering belongs to the surrounding frame engine; retained envelope history does ...
Definition spectral_cross_synthesis.hpp:303
bool process(const std::complex< SampleType > *const *carrier, const std::complex< SampleType > *const *modulator, std::complex< SampleType > *const *output, int channels, int num_bins) noexcept
Cross-synthesize one coherent frame group.
Definition spectral_cross_synthesis.hpp:207
static constexpr float maximum_transfer_db
Definition spectral_cross_synthesis.hpp:70
@ partial
Inspectable or editable, but not fully rebuildable or playable.
Definition component.hpp:176
@ count
sentinel: number of real capabilities (keep LAST)
Definition canvas_capability.hpp:38
@ b
Definition pitch.hpp:31
@ index
Definition gpu_pass.hpp:140
Definition additive_bank.hpp:218
@ Ok
Definition source_filter_analysis.hpp:29
SpectralCrossSynthesisConfigT< float > SpectralCrossSynthesisConfig
Definition spectral_cross_synthesis.hpp:445
SpectralCrossSynthesisConfigT< double > SpectralCrossSynthesisConfig64
Definition spectral_cross_synthesis.hpp:446
SpectralCrossSynthesisT< double > SpectralCrossSynthesis64
Definition spectral_cross_synthesis.hpp:448
@ c
Definition spectrum_trace.hpp:35
constexpr int kSpectralFrameEngineMaximumFftSize
Definition spectral_frame_engine.hpp:94
bool checked_capacity_product(std::uint64_t lhs, std::uint64_t rhs, std::uint64_t maximum, std::uint64_t &product) noexcept
Definition checked_allocation.hpp:12
@ ratio
base_frequency_hz * ratio.
Definition fm_operator_engine.hpp:20
SpectralCrossSynthesisPrepareConfigT< float > SpectralCrossSynthesisPrepareConfig
Definition spectral_cross_synthesis.hpp:443
@ maximum
Definition spectrum_trace.hpp:34
SpectralCrossSynthesisNormalization
Definition spectral_cross_synthesis.hpp:31
@ none
Definition spectral_cross_synthesis.hpp:32
@ match_carrier_rms
Definition spectral_cross_synthesis.hpp:33
constexpr std::uint64_t kTargetAddressMaximumBytes
Definition checked_allocation.hpp:9
@ a
Definition formant_filter_bank.hpp:20
@ e
Definition formant_filter_bank.hpp:20
constexpr int kSpectralFrameEngineMinimumFftSize
Definition spectral_frame_engine.hpp:93
@ none
Definition dither.hpp:17
SpectralCrossSynthesisPrepareConfigT< double > SpectralCrossSynthesisPrepareConfig64
Definition spectral_cross_synthesis.hpp:444
SpectralCrossSynthesisT< float > SpectralCrossSynthesis
Definition spectral_cross_synthesis.hpp:447
float sum(const float *data, std::size_t count) noexcept
void scale(const float *a, float scalar, float *dst, std::size_t count) noexcept
@ value
the widget's primary value (Knob, Fader, RangeSlider, Toggle, ProgressBar)
Definition binding_diagnostics.hpp:19
Prepared, allocation-free spectral-envelope and linear-prediction analysis.
Streaming STFT analysis + weighted overlap-add (WOLA) synthesis for DSP-grade spectral processing — d...
int true_envelope_iterations
Definition source_filter_analysis.hpp:65
int fft_size
Definition source_filter_analysis.hpp:63
SampleType convergence_tolerance
Maximum remaining positive log-magnitude residual.
Definition source_filter_analysis.hpp:68
int order
Definition source_filter_analysis.hpp:64
Definition spectral_cross_synthesis.hpp:45
SpectralCrossSynthesisNormalization normalization
Definition spectral_cross_synthesis.hpp:56
float mix
Definition spectral_cross_synthesis.hpp:49
float amount
Definition spectral_cross_synthesis.hpp:47
int envelope_smoothing_frames
Definition spectral_cross_synthesis.hpp:51
float magnitude_floor
Definition spectral_cross_synthesis.hpp:55
float maximum_transfer_db
Definition spectral_cross_synthesis.hpp:53
Definition spectral_cross_synthesis.hpp:36
int true_envelope_iterations
Definition spectral_cross_synthesis.hpp:41
int channels
Definition spectral_cross_synthesis.hpp:37
int lifter_order
Definition spectral_cross_synthesis.hpp:40
int fft_size
Definition spectral_cross_synthesis.hpp:38
float convergence_tolerance
Definition spectral_cross_synthesis.hpp:42