Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
spectral_envelope_shifter.hpp
Go to the documentation of this file.
1#pragma once
2
28
29#include <algorithm>
30#include <cassert>
31#include <cmath>
32#include <complex>
34#include <vector>
35
36namespace pulp::signal {
37
39 int fft_size = 2048;
42 int order = 0;
48 float max_gain_db = 60.0f;
49};
50
51template <typename SampleType = float> class SpectralEnvelopeShifterT {
52 public:
54
55 static bool checked_retained_bytes(int fft_size, std::uint64_t target_max_bytes,
56 std::uint64_t& bytes) noexcept {
57 const auto bins = static_cast<std::uint64_t>(fft_size / 2 + 1);
59 analysis_config.fft_size = fft_size;
60 analysis_config.order = fft_size / 16;
61 std::uint64_t analysis_bytes = 0;
62 CheckedRetainedByteCharge charge(target_max_bytes);
63 if (!charge.add<SampleType>(bins) || !charge.add<SampleType>(bins) ||
65 analysis_config, target_max_bytes, analysis_bytes) ||
66 !charge.add_retained_bytes(analysis_bytes))
67 return false;
68 bytes = charge.total();
69 return true;
70 }
71
77 auto effective_config = config;
78 if (!std::isfinite(effective_config.max_gain_db) || effective_config.max_gain_db < 0.0f)
80 if (effective_config.order <= 0)
81 effective_config.order = effective_config.fft_size / 16;
83 analysis_config.fft_size = effective_config.fft_size;
84 analysis_config.order = effective_config.order;
85 analysis_config.true_envelope_iterations = effective_config.true_envelope_iterations;
86 analysis_config.convergence_tolerance = SampleType{0};
88 const auto analysis_status = next_analyzer.prepare(analysis_config);
89 if (analysis_status != SourceFilterAnalysisStatus::Ok)
90 return analysis_status;
91
92 const auto commit = [&]() {
93 const auto next_num_bins = effective_config.fft_size / 2 + 1;
94 std::vector<SampleType> next_log_mag(static_cast<size_t>(next_num_bins), SampleType{0});
95 std::vector<SampleType> next_envelope(static_cast<size_t>(next_num_bins),
96 SampleType{0});
97 config_ = effective_config;
98 num_bins_ = next_num_bins;
99 envelope_analyzer_ = std::move(next_analyzer);
100 log_mag_ = std::move(next_log_mag);
101 envelope_ = std::move(next_envelope);
102 max_gain_ln_ = static_cast<SampleType>(effective_config.max_gain_db * 0.1151293f);
104 };
105#if defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)
106 try {
107 return commit();
108 } catch (const std::bad_alloc&) {
110 } catch (const std::length_error&) {
112 }
113#else
114 return commit();
115#endif
116 }
117
118 int num_bins() const {
119 return num_bins_;
120 }
121 int order() const {
122 return config_.order;
123 }
124
128 void process_group(std::complex<SampleType>* const* frames, int channels, int num_bins,
129 SampleType warp) {
130 assert(num_bins == num_bins_);
131 assert(channels >= 1);
132 if (!std::isfinite(warp) || warp <= SampleType{0})
133 return;
134 if (warp == SampleType{1})
135 return; // exact bypass — neutral by construction
136
137 // Group RMS magnitude → log domain, floored 40 dB below the frame
138 // peak: without the relative floor, the near-zero bins between
139 // harmonics drag the cepstral envelope far below the harmonic
140 // tops and the true-envelope refinement cannot recover the
141 // contrast within a bounded iteration budget. Envelope detail
142 // more than 40 dB under the frame peak has no audible effect on
143 // the correction.
144 SampleType max_power = SampleType{0};
145 double energy_before = 0.0;
146 for (int k = 0; k < num_bins_; ++k) {
147 SampleType power = SampleType{0};
148 for (int ch = 0; ch < channels; ++ch) {
149 const SampleType re = frames[ch][k].real();
150 const SampleType im = frames[ch][k].imag();
151 power += re * re + im * im;
152 }
153 energy_before += static_cast<double>(power);
154 log_mag_[static_cast<size_t>(k)] = power / static_cast<SampleType>(channels);
155 max_power = std::max(max_power, log_mag_[static_cast<size_t>(k)]);
156 }
157 const SampleType floor_power =
158 std::max(max_power * static_cast<SampleType>(1e-4), static_cast<SampleType>(1e-24));
159 for (int k = 0; k < num_bins_; ++k)
160 log_mag_[static_cast<size_t>(k)] =
161 static_cast<SampleType>(0.5) *
162 std::log(std::max(log_mag_[static_cast<size_t>(k)], floor_power));
163
164 const auto envelope_result = envelope_analyzer_.estimate(log_mag_, envelope_);
165 assert(envelope_result.ok());
166 if (!envelope_result.ok())
167 return;
168
169 // Apply E(k*warp) / E(k) in the log domain with linear
170 // interpolation, clamped at the spectrum edges and by max gain.
171 double energy_after = 0.0;
172 for (int k = 0; k < num_bins_; ++k) {
173 const SampleType pos =
174 std::min(static_cast<SampleType>(k) * warp, static_cast<SampleType>(num_bins_ - 1));
175 const int i0 = static_cast<int>(pos);
176 const int i1 = std::min(i0 + 1, num_bins_ - 1);
177 const SampleType frac = pos - static_cast<SampleType>(i0);
178 const SampleType warped = envelope_[static_cast<size_t>(i0)] * (SampleType{1} - frac) +
179 envelope_[static_cast<size_t>(i1)] * frac;
180 SampleType gain_ln = warped - envelope_[static_cast<size_t>(k)];
181 gain_ln = std::clamp(gain_ln, -max_gain_ln_, max_gain_ln_);
182 const SampleType gain = std::exp(gain_ln);
183 for (int ch = 0; ch < channels; ++ch) {
184 frames[ch][k] *= gain;
185 energy_after += static_cast<double>(std::norm(frames[ch][k]));
186 }
187 }
188
189 // Energy-preserving normalization. The correction RESHAPES the spectral
190 // envelope (the whole point — moving formants) but must not change
191 // overall loudness. Without this, a narrow-band input (e.g. a single
192 // tone, whose cepstral envelope peaks AT the tone) is scaled by the
193 // falling envelope sampled above the peak, so the level collapses
194 // progressively as warp (= pitch ratio) grows — silent pitch-up with
195 // formant preservation ON. Restoring the pre-correction energy keeps the
196 // timbre change while holding loudness constant.
197 //
198 // SAFETY: only normalize frames with real energy, and CLAMP the gain.
199 // On a near-silent frame (the gaps between instrument notes) the ratio
200 // energy_before/energy_after is ill-conditioned and could explode to a
201 // huge gain → Inf/NaN → which a host (Logic) treats as a dead channel
202 // and mutes the whole signal path. A sane correction never needs more
203 // than a few dB of overall trim, so clamp to +/-18 dB and leave silent
204 // frames untouched.
205 constexpr double kEnergyFloor = 1e-9; // below this the frame is silence
206 if (energy_after > kEnergyFloor && energy_before > kEnergyFloor) {
207 SampleType norm = static_cast<SampleType>(std::sqrt(energy_before / energy_after));
208 // Generous bound: a narrow-band tone legitimately needs up to ~+30 dB
209 // of correction (its cepstral envelope concentrates the energy), so
210 // the guard rail must clear that with margin. It exists only to cap a
211 // runaway toward Inf on a numerically-degenerate frame, NOT to limit
212 // a real correction — a tight rail under-corrects and re-introduces
213 // the quiet-pitch-up bug. +/-48 dB.
214 norm = std::clamp(norm, static_cast<SampleType>(1.0 / 256.0),
215 static_cast<SampleType>(256.0));
216 if (std::isfinite(static_cast<double>(norm))) {
217 for (int k = 0; k < num_bins_; ++k)
218 for (int ch = 0; ch < channels; ++ch)
219 frames[ch][k] *= norm;
220 }
221 }
222 }
223
224 private:
226 CepstralEnvelopeAnalyzerT<SampleType> envelope_analyzer_;
227 int num_bins_ = 0;
228 SampleType max_gain_ln_ = static_cast<SampleType>(6.9);
229
230 std::vector<SampleType> log_mag_;
231 std::vector<SampleType> envelope_;
232};
233
236
237} // 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
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_envelope_shifter.hpp:51
int order() const
Definition spectral_envelope_shifter.hpp:121
void process_group(std::complex< SampleType > *const *frames, int channels, int num_bins, SampleType warp)
Estimate the group envelope and rescale all channels' bins by E(k * warp) / E(k).
Definition spectral_envelope_shifter.hpp:128
int num_bins() const
Definition spectral_envelope_shifter.hpp:118
static bool checked_retained_bytes(int fft_size, std::uint64_t target_max_bytes, std::uint64_t &bytes) noexcept
Definition spectral_envelope_shifter.hpp:55
SourceFilterAnalysisStatus prepare(const SpectralEnvelopeShifterConfig &config)
RT contract: prepare() allocates FFT and scratch/envelope storage and is not audio-thread safe.
Definition spectral_envelope_shifter.hpp:76
Definition additive_bank.hpp:218
SourceFilterAnalysisStatus
Definition source_filter_analysis.hpp:28
@ InvalidGain
Definition source_filter_analysis.hpp:46
@ Ok
Definition source_filter_analysis.hpp:29
@ AllocationFailure
Definition source_filter_analysis.hpp:38
@ e
Definition formant_filter_bank.hpp:20
SpectralEnvelopeShifterT< double > SpectralEnvelopeShifter64
Definition spectral_envelope_shifter.hpp:235
SpectralEnvelopeShifterT< float > SpectralEnvelopeShifter
Definition spectral_envelope_shifter.hpp:234
Prepared, allocation-free spectral-envelope and linear-prediction analysis.
Definition source_filter_analysis.hpp:60
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_envelope_shifter.hpp:38
int fft_size
Definition spectral_envelope_shifter.hpp:39
int order
Cepstral cutoff (quefrency bins kept).
Definition spectral_envelope_shifter.hpp:42
float max_gain_db
Finite, nonnegative gain clamp for the applied correction, in dB (safety bound for degenerate envelop...
Definition spectral_envelope_shifter.hpp:48
int true_envelope_iterations
True-envelope refinement passes (0 = plain liftering).
Definition spectral_envelope_shifter.hpp:45