Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
spectral_delay_matrix.hpp
Go to the documentation of this file.
1#pragma once
2
11
16
17#include <algorithm>
18#include <cmath>
19#include <complex>
20#include <cstddef>
21#include <cstdint>
22#include <limits>
23#include <memory>
24#include <optional>
25#include <type_traits>
26#include <utility>
27#include <vector>
28
29namespace pulp::signal {
30
31inline constexpr std::size_t kSpectralDelayMinimumRetainedBytes = std::size_t{1} << 20u;
32inline constexpr std::size_t kSpectralDelayMaximumRetainedBytes = std::size_t{1} << 29u;
33
35 float freq01 = 0.0f;
36 float delay_ms = 0.0f;
37 float attenuation_db = 0.0f;
38};
39
41 std::uint64_t version = 0;
42 std::vector<SpectralDelayBreakpoint> breakpoints;
43};
44
46 std::uint64_t version = 0;
47 int num_bins = 0;
48 std::vector<std::int32_t> delay_frames;
49 std::vector<float> attenuation_linear;
50};
51
54 double sample_rate = 48000.0;
55 double max_delay_ms = 2000.0;
56 std::size_t max_retained_bytes = std::size_t{1} << 26u;
57};
58
59template <typename SampleType> struct SpectralDelayMatrixGeometry {
60 int num_bins = 0;
62 std::uint64_t retained_bytes = 0;
63};
64
65template <typename SampleType>
66inline std::optional<SpectralDelayMatrixGeometry<SampleType>>
68 const SpectralDelayMatrixConfig& config,
69 std::uint64_t target_max_bytes = kTargetAddressMaximumBytes) noexcept {
70 static_assert(std::is_floating_point_v<SampleType>);
71 if (!std::isfinite(config.sample_rate) || config.sample_rate <= 0.0 ||
72 !std::isfinite(config.max_delay_ms) || config.max_delay_ms < 0.0 ||
73 config.max_delay_ms > 60000.0 ||
74 config.max_retained_bytes < kSpectralDelayMinimumRetainedBytes ||
75 config.max_retained_bytes > kSpectralDelayMaximumRetainedBytes)
76 return std::nullopt;
77 const auto engine =
78 checked_spectral_frame_engine_geometry<SampleType>(config.frame, target_max_bytes);
79 if (!engine)
80 return std::nullopt;
81
82 const double frames_exact =
83 config.max_delay_ms * config.sample_rate / (1000.0 * config.frame.analysis_hop);
84 if (!std::isfinite(frames_exact) ||
85 frames_exact > static_cast<double>(std::numeric_limits<int>::max() - 1))
86 return std::nullopt;
87 const auto history_frames = static_cast<std::uint64_t>(std::floor(frames_exact)) + 1u;
88 std::uint64_t channel_frames = 0;
89 std::uint64_t history_elements = 0;
90 if (!checked_capacity_product(static_cast<std::uint64_t>(config.frame.channels), history_frames,
91 UINT64_MAX, channel_frames) ||
92 !checked_capacity_product(channel_frames, static_cast<std::uint64_t>(engine->num_bins),
93 UINT64_MAX, history_elements))
94 return std::nullopt;
95
96 CheckedRetainedByteCharge charge(target_max_bytes);
97 if (!charge.add_retained_bytes(engine->retained_bytes) ||
98 !charge.add<std::complex<SampleType>>(history_elements) ||
99 !charge.add_repeated<std::int32_t>(static_cast<std::uint64_t>(engine->num_bins), 3) ||
100 !charge.add_repeated<float>(static_cast<std::uint64_t>(engine->num_bins), 3))
101 return std::nullopt;
102 const auto retained = charge.total();
103 if (retained > config.max_retained_bytes)
104 return std::nullopt;
105 return SpectralDelayMatrixGeometry<SampleType>{engine->num_bins,
106 static_cast<int>(history_frames), retained};
107}
108
110 int num_bins, int analysis_hop, double sample_rate,
111 int maximum_delay_frames,
112 SpectralDelayMatrixTable& out_table) {
113 if (recipe.breakpoints.empty() || recipe.breakpoints.size() > 256 || num_bins < 2 ||
114 analysis_hop <= 0 || maximum_delay_frames < 0 || !std::isfinite(sample_rate) ||
115 sample_rate <= 0.0)
116 return false;
117 for (std::size_t i = 0; i < recipe.breakpoints.size(); ++i) {
118 const auto& point = recipe.breakpoints[i];
119 if (!std::isfinite(point.freq01) || !std::isfinite(point.delay_ms) ||
120 !std::isfinite(point.attenuation_db) || point.freq01 < 0.0f || point.freq01 > 1.0f ||
121 (i && !(recipe.breakpoints[i - 1].freq01 < point.freq01)))
122 return false;
123 }
124
125 const double maximum_delay_ms =
126 static_cast<double>(maximum_delay_frames) * 1000.0 * analysis_hop / sample_rate;
127 SpectralDelayMatrixTable candidate;
128 candidate.version = recipe.version;
129 candidate.num_bins = num_bins;
130 candidate.delay_frames.resize(static_cast<std::size_t>(num_bins));
131 candidate.attenuation_linear.resize(static_cast<std::size_t>(num_bins));
132 for (int bin = 0; bin < num_bins; ++bin) {
133 const float freq01 = static_cast<float>(bin) / static_cast<float>(num_bins - 1);
134 auto upper = std::upper_bound(
135 recipe.breakpoints.begin(), recipe.breakpoints.end(), freq01,
136 [](float value, const SpectralDelayBreakpoint& point) { return value < point.freq01; });
137 const SpectralDelayBreakpoint* lo = nullptr;
138 const SpectralDelayBreakpoint* hi = nullptr;
139 if (upper == recipe.breakpoints.begin())
140 lo = hi = &recipe.breakpoints.front();
141 else if (upper == recipe.breakpoints.end())
142 lo = hi = &recipe.breakpoints.back();
143 else {
144 hi = &*upper;
145 lo = &*(upper - 1);
146 }
147 const double mix =
148 lo == hi ? 0.0 : (freq01 - lo->freq01) / static_cast<double>(hi->freq01 - lo->freq01);
149 const double delay_ms =
150 std::clamp(lo->delay_ms + mix * (hi->delay_ms - lo->delay_ms), 0.0, maximum_delay_ms);
151 const double attenuation_db = std::clamp(
152 lo->attenuation_db + mix * (hi->attenuation_db - lo->attenuation_db), -96.0, 24.0);
153 const auto delay = std::llround(delay_ms * sample_rate / (1000.0 * analysis_hop));
154 candidate.delay_frames[static_cast<std::size_t>(bin)] =
155 static_cast<std::int32_t>(std::clamp<long long>(delay, 0, maximum_delay_frames));
156 candidate.attenuation_linear[static_cast<std::size_t>(bin)] =
157 static_cast<float>(std::pow(10.0, attenuation_db / 20.0));
158 }
159 out_table = std::move(candidate);
160 return true;
161}
162
163template <typename SampleType = float> class SpectralDelayMatrixT {
164 public:
165 static_assert(std::is_floating_point_v<SampleType>);
166
167 [[nodiscard]] bool prepare(const SpectralDelayMatrixConfig& config) {
168 const auto geometry = checked_spectral_delay_matrix_geometry<SampleType>(config);
169 if (!geometry)
170 return false;
171 auto candidate = std::make_unique<PreparedState>();
172 candidate->config = config;
173 candidate->geometry = *geometry;
174 candidate->engine.prepare(config.frame);
175 candidate->history.assign(static_cast<std::size_t>(config.frame.channels) *
176 static_cast<std::size_t>(geometry->history_frames) *
177 static_cast<std::size_t>(geometry->num_bins),
178 {});
180 identity.num_bins = geometry->num_bins;
181 identity.delay_frames.assign(static_cast<std::size_t>(geometry->num_bins), 0);
182 identity.attenuation_linear.assign(static_cast<std::size_t>(geometry->num_bins), 1.0f);
183 candidate->publication =
184 std::make_unique<runtime::TripleBuffer<PublishedTable>>(PublishedTable{identity, 0});
185 state_ = std::move(candidate);
186 next_generation_ = 1;
187 return true;
188 }
189
190 static bool compile_table(const SpectralDelayMatrixRecipe& recipe, int num_bins,
191 int analysis_hop, double sample_rate, int maximum_delay_frames,
192 SpectralDelayMatrixTable& out_table) {
193 return compile_spectral_delay_matrix_table(recipe, num_bins, analysis_hop, sample_rate,
194 maximum_delay_frames, out_table);
195 }
196
197 [[nodiscard]] bool publish_table(const SpectralDelayMatrixTable& table) {
198 if (!state_ || !valid_table(table))
199 return false;
200 const auto generation = next_generation_++;
201 if (next_generation_ == 0)
202 ++next_generation_;
203 state_->publication->write_with([&](PublishedTable& payload) {
204 payload.table.version = table.version;
205 payload.table.num_bins = table.num_bins;
206 std::copy(table.delay_frames.begin(), table.delay_frames.end(),
207 payload.table.delay_frames.begin());
208 std::copy(table.attenuation_linear.begin(), table.attenuation_linear.end(),
209 payload.table.attenuation_linear.begin());
210 payload.generation = generation;
211 });
212 state_->published_generation.write(generation);
213 return true;
214 }
215
216 void process(const SampleType* const* input, SampleType* const* output,
217 int num_samples) noexcept {
218 if (!state_ || num_samples <= 0)
219 return;
220 state_->engine.process(input, output, num_samples,
221 [this](std::complex<SampleType>* const* frames, int bins) noexcept {
222 process_frame(frames, bins);
223 });
224 }
225
227 void purge() noexcept {
228 if (!state_)
229 return;
230 std::fill(state_->history.begin(), state_->history.end(), std::complex<SampleType>{});
231 state_->frames_seen = 0;
232 state_->write_frame = 0;
233 }
234
235 void reset() noexcept {
236 if (!state_)
237 return;
238 purge();
239 state_->engine.reset();
240 }
241
243 [[nodiscard]] int latency_samples() const noexcept {
244 return state_ ? state_->engine.latency_samples() : 0;
245 }
246 [[nodiscard]] int max_delay_frames() const noexcept {
247 return state_ ? state_->geometry.history_frames - 1 : 0;
248 }
249 [[nodiscard]] int num_bins() const noexcept {
250 return state_ ? state_->geometry.num_bins : 0;
251 }
252 [[nodiscard]] std::uint64_t retained_bytes() const noexcept {
253 return state_ ? state_->geometry.retained_bytes : 0;
254 }
255 [[nodiscard]] std::uint64_t maximum_tail_samples() const noexcept {
256 return state_ ? static_cast<std::uint64_t>(latency_samples()) +
257 static_cast<std::uint64_t>(max_delay_frames()) *
258 state_->config.frame.analysis_hop
259 : 0;
260 }
261 [[nodiscard]] std::uint64_t active_table_version() const noexcept {
262 return state_ ? state_->active_version.read() : 0;
263 }
264 [[nodiscard]] bool table_publication_pending() const noexcept {
265 return state_ && state_->published_generation.read() != state_->active_generation.read();
266 }
267
268 private:
269 struct PublishedTable {
271 std::uint64_t generation = 0;
272 };
273 struct PreparedState {
274 SpectralDelayMatrixConfig config{};
275 SpectralDelayMatrixGeometry<SampleType> geometry{};
276 SpectralFrameEngineT<SampleType> engine;
277 std::vector<std::complex<SampleType>> history;
278 std::unique_ptr<runtime::TripleBuffer<PublishedTable>> publication;
279 runtime::SeqLock<std::uint64_t> published_generation;
280 runtime::SeqLock<std::uint64_t> active_generation;
281 runtime::SeqLock<std::uint64_t> active_version;
282 std::uint64_t consumed_generation = 0;
283 std::uint64_t frames_seen = 0;
284 int write_frame = 0;
285 };
286
287 bool valid_table(const SpectralDelayMatrixTable& table) const noexcept {
288 const auto bins = static_cast<std::size_t>(state_->geometry.num_bins);
289 if (table.num_bins != state_->geometry.num_bins || table.delay_frames.size() != bins ||
290 table.attenuation_linear.size() != bins)
291 return false;
292 for (std::size_t i = 0; i < bins; ++i)
293 if (table.delay_frames[i] < 0 || table.delay_frames[i] > max_delay_frames() ||
294 !std::isfinite(table.attenuation_linear[i]) || table.attenuation_linear[i] < 0.0f)
295 return false;
296 return true;
297 }
298 std::size_t offset(int channel, int frame, int bin) const noexcept {
299 return (static_cast<std::size_t>(channel) * state_->geometry.history_frames +
300 static_cast<std::size_t>(frame)) *
301 state_->geometry.num_bins +
302 static_cast<std::size_t>(bin);
303 }
304 void process_frame(std::complex<SampleType>* const* frames, int bins) noexcept {
305 const auto& publication = state_->publication->read();
306 if (state_->consumed_generation != publication.generation) {
307 state_->consumed_generation = publication.generation;
308 state_->active_generation.write(publication.generation);
309 state_->active_version.write(publication.table.version);
310 }
311 const auto& table = publication.table;
312 const int history_frames = state_->geometry.history_frames;
313 for (int channel = 0; channel < state_->config.frame.channels; ++channel) {
314 for (int bin = 0; bin < bins; ++bin) {
315 state_->history[offset(channel, state_->write_frame, bin)] = frames[channel][bin];
316 const int delay = table.delay_frames[static_cast<std::size_t>(bin)];
317 if (static_cast<std::uint64_t>(delay) > state_->frames_seen) {
318 frames[channel][bin] = {};
319 } else {
320 const int read =
321 (state_->write_frame - delay + history_frames) % history_frames;
322 frames[channel][bin] =
323 state_->history[offset(channel, read, bin)] *
324 static_cast<SampleType>(
325 table.attenuation_linear[static_cast<std::size_t>(bin)]);
326 }
327 }
328 }
329 state_->write_frame = (state_->write_frame + 1) % history_frames;
330 ++state_->frames_seen;
331 }
332
333 std::unique_ptr<PreparedState> state_;
334 std::uint64_t next_generation_ = 1;
335};
336
339
340} // namespace pulp::signal
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
bool add_repeated(std::uint64_t elements_per_allocation, std::uint64_t repetitions) noexcept
Definition checked_allocation.hpp:70
Definition spectral_delay_matrix.hpp:163
static bool compile_table(const SpectralDelayMatrixRecipe &recipe, int num_bins, int analysis_hop, double sample_rate, int maximum_delay_frames, SpectralDelayMatrixTable &out_table)
Definition spectral_delay_matrix.hpp:190
int num_bins() const noexcept
Definition spectral_delay_matrix.hpp:249
void reset() noexcept
Clear delayed content and the underlying analysis/synthesis pipeline.
Definition spectral_delay_matrix.hpp:235
int max_delay_frames() const noexcept
Definition spectral_delay_matrix.hpp:246
std::uint64_t retained_bytes() const noexcept
Definition spectral_delay_matrix.hpp:252
bool publish_table(const SpectralDelayMatrixTable &table)
Definition spectral_delay_matrix.hpp:197
int latency_samples() const noexcept
Fixed engine latency only; per-bin delays are declared content timing.
Definition spectral_delay_matrix.hpp:243
void purge() noexcept
Clear only delayed spectral content; preserve the engine pipeline and table.
Definition spectral_delay_matrix.hpp:227
void process(const SampleType *const *input, SampleType *const *output, int num_samples) noexcept
Definition spectral_delay_matrix.hpp:216
std::uint64_t maximum_tail_samples() const noexcept
Definition spectral_delay_matrix.hpp:255
std::uint64_t active_table_version() const noexcept
Definition spectral_delay_matrix.hpp:261
bool table_publication_pending() const noexcept
Definition spectral_delay_matrix.hpp:264
bool prepare(const SpectralDelayMatrixConfig &config)
Definition spectral_delay_matrix.hpp:167
std::uint64_t read(std::span< const std::uint8_t > b, std::size_t p, unsigned n) noexcept
Definition spectral_frame_codec.hpp:111
Definition additive_bank.hpp:218
std::optional< SpectralFrameEngineGeometry > checked_spectral_frame_engine_geometry(const SpectralFrameEngineConfig &config, std::uint64_t target_max_bytes=kTargetAddressMaximumBytes) noexcept
Derive every integer and typed backing-store capacity used by prepare().
Definition spectral_frame_engine.hpp:151
constexpr std::size_t kSpectralDelayMinimumRetainedBytes
Definition spectral_delay_matrix.hpp:31
SpectralDelayMatrixT< double > SpectralDelayMatrix64
Definition spectral_delay_matrix.hpp:338
@ delay
Definition envelope.hpp:57
std::optional< SpectralDelayMatrixGeometry< SampleType > > checked_spectral_delay_matrix_geometry(const SpectralDelayMatrixConfig &config, std::uint64_t target_max_bytes=kTargetAddressMaximumBytes) noexcept
Definition spectral_delay_matrix.hpp:67
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
constexpr std::size_t kSpectralDelayMaximumRetainedBytes
Definition spectral_delay_matrix.hpp:32
SpectralDelayMatrixT< float > SpectralDelayMatrix
Definition spectral_delay_matrix.hpp:337
constexpr std::uint64_t kTargetAddressMaximumBytes
Definition checked_allocation.hpp:9
@ i
Definition formant_filter_bank.hpp:20
bool compile_spectral_delay_matrix_table(const SpectralDelayMatrixRecipe &recipe, int num_bins, int analysis_hop, double sample_rate, int maximum_delay_frames, SpectralDelayMatrixTable &out_table)
Definition spectral_delay_matrix.hpp:109
Lock-free sequence-lock for coherent multi-field snapshots.
Streaming STFT analysis + weighted overlap-add (WOLA) synthesis for DSP-grade spectral processing — d...
Definition spectral_delay_matrix.hpp:34
float freq01
Definition spectral_delay_matrix.hpp:35
float attenuation_db
Definition spectral_delay_matrix.hpp:37
float delay_ms
Definition spectral_delay_matrix.hpp:36
Definition spectral_delay_matrix.hpp:52
std::size_t max_retained_bytes
Definition spectral_delay_matrix.hpp:56
SpectralFrameEngineConfig frame
Definition spectral_delay_matrix.hpp:53
double sample_rate
Definition spectral_delay_matrix.hpp:54
double max_delay_ms
Definition spectral_delay_matrix.hpp:55
Definition spectral_delay_matrix.hpp:59
int num_bins
Definition spectral_delay_matrix.hpp:60
int history_frames
Definition spectral_delay_matrix.hpp:61
std::uint64_t retained_bytes
Definition spectral_delay_matrix.hpp:62
Definition spectral_delay_matrix.hpp:40
std::vector< SpectralDelayBreakpoint > breakpoints
Definition spectral_delay_matrix.hpp:42
std::uint64_t version
Definition spectral_delay_matrix.hpp:41
Definition spectral_delay_matrix.hpp:45
std::vector< float > attenuation_linear
Definition spectral_delay_matrix.hpp:49
std::uint64_t version
Definition spectral_delay_matrix.hpp:46
int num_bins
Definition spectral_delay_matrix.hpp:47
std::vector< std::int32_t > delay_frames
Definition spectral_delay_matrix.hpp:48
Configuration for a SpectralFrameEngine.
Definition spectral_frame_engine.hpp:56
Lock-free latest-value publication between two threads.