Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
spectral_frame_engine.hpp
Go to the documentation of this file.
1#pragma once
2
38
40#include <pulp/signal/fft.hpp>
42#include <algorithm>
43#include <cassert>
44#include <complex>
45#include <cmath>
46#include <cstdint>
47#include <limits>
48#include <optional>
49#include <type_traits>
50#include <utility>
51#include <vector>
52
53namespace pulp::signal {
54
57 int fft_size = 2048; // Power of 2, 256–16384
58 int analysis_hop = 512; // Samples between analysis frames (<= fft_size/2)
59 int channels = 1; // Channel-group size (>= 1)
60 int max_block = 4096; // Largest process() block the caller will use
61 int max_synthesis_hop = 0; // 0 = 2 * analysis_hop; upper bound for synthesize_frame()
75 bool defer_synthesis = true;
78 bool guarded_split = false;
79};
80
92
93inline constexpr int kSpectralFrameEngineMinimumFftSize = 256;
94inline constexpr int kSpectralFrameEngineMaximumFftSize = 16384;
95inline constexpr std::uint64_t kSpectralFrameEngineMaximumRingSize =
96 std::uint64_t{1} << 30u;
97
100 int num_bins = 0;
101 int ring_size = 0;
102 std::uint64_t input_ring_elements = 0;
103 std::uint64_t output_ring_elements = 0;
104 std::uint64_t frame_elements = 0;
105 std::uint64_t retained_bytes = 0;
106};
107
108inline bool is_valid_spectral_frame_geometry(int fft_size, int analysis_hop) noexcept {
109 return fft_size >= kSpectralFrameEngineMinimumFftSize
111 && (fft_size & (fft_size - 1)) == 0
112 && analysis_hop > 0
113 && analysis_hop <= fft_size / 2;
114}
115
119inline constexpr std::int64_t
120spectral_frame_engine_first_frame_start(int fft_size, int analysis_hop,
121 bool full_overlap_stream_start) noexcept {
122 if (!full_overlap_stream_start || fft_size <= 0 || analysis_hop <= 0)
123 return 0;
124 return -static_cast<std::int64_t>((fft_size - 1) / analysis_hop) * analysis_hop;
125}
126
131template <typename SampleType>
132inline double spectral_ola_window_energy(const SampleType* window, int fft_size, int hop,
133 std::int64_t first_frame_start,
134 std::int64_t position) noexcept {
135 if (window == nullptr || fft_size <= 0 || hop <= 0 || position < first_frame_start)
136 return 0.0;
137 const std::int64_t lowest =
138 std::max(first_frame_start, position - static_cast<std::int64_t>(fft_size) + 1);
139 const std::int64_t first_k = (lowest - first_frame_start + hop - 1) / hop;
140 double sum = 0.0;
141 for (std::int64_t start = first_frame_start + first_k * hop; start <= position; start += hop) {
142 const auto w = static_cast<double>(window[static_cast<std::size_t>(position - start)]);
143 sum += w * w;
144 }
145 return sum;
146}
147
150template <typename SampleType>
151inline std::optional<SpectralFrameEngineGeometry> checked_spectral_frame_engine_geometry(
152 const SpectralFrameEngineConfig& config,
153 std::uint64_t target_max_bytes = kTargetAddressMaximumBytes) noexcept {
154 static_assert(std::is_floating_point_v<SampleType>);
155 if (!is_valid_spectral_frame_geometry(config.fft_size, config.analysis_hop)
156 || config.channels < 1 || config.max_block <= 0)
157 return std::nullopt;
158 if (config.guarded_split && config.defer_synthesis)
159 return std::nullopt;
160
161 const int max_synthesis_hop = config.max_synthesis_hop > 0
162 ? config.max_synthesis_hop
163 : 2 * config.analysis_hop;
164 if (max_synthesis_hop <= 0) return std::nullopt;
165
166 const auto fft_size = static_cast<std::uint64_t>(config.fft_size);
167 const auto channels = static_cast<std::uint64_t>(config.channels);
168 const auto max_block = static_cast<std::uint64_t>(config.max_block);
169 const auto synthesis_hop = static_cast<std::uint64_t>(max_synthesis_hop);
170 const auto worst_frames = max_block / static_cast<std::uint64_t>(config.analysis_hop) + 2u;
171
172 std::uint64_t backlog = 0;
173 std::uint64_t ring_span = 0;
174 if (!checked_capacity_product(worst_frames, synthesis_hop,
176 || !checked_capacity_sum(fft_size, max_block,
178 || !checked_capacity_sum(ring_span, backlog,
180 return std::nullopt;
181
182 std::uint64_t ring_size = 1;
183 while (ring_size < ring_span) ring_size <<= 1u;
184
185 const auto num_bins = fft_size / 2u + 1u;
187 if (!checked_capacity_product(channels, fft_size,
188 std::numeric_limits<std::uint64_t>::max(),
189 geometry.input_ring_elements)
190 || !checked_capacity_product(channels, ring_size,
191 std::numeric_limits<std::uint64_t>::max(),
192 geometry.output_ring_elements)
193 || !checked_capacity_product(channels, num_bins,
194 std::numeric_limits<std::uint64_t>::max(),
195 geometry.frame_elements))
196 return std::nullopt;
197
198 CheckedRetainedByteCharge charge(target_max_bytes);
199 std::uint64_t fft_retained_bytes = 0;
200 if (!charge.add<SampleType>(geometry.input_ring_elements)
201 || !charge.add<SampleType>(geometry.output_ring_elements)
202 || !charge.add<SampleType>(ring_size) // norm ring
203 || !charge.add<SampleType>(fft_size) // window
204 || !charge.add<SampleType>(fft_size) // time scratch
205 || !charge.add<std::complex<SampleType>>(geometry.frame_elements)
206 || !charge.add<std::complex<SampleType>*>(channels) // frame pointers
207 || !checked_fft_retained_bytes<SampleType>(fft_size, target_max_bytes,
208 fft_retained_bytes)
209 || !charge.add_retained_bytes(fft_retained_bytes))
210 return std::nullopt;
211 if (config.guarded_split && (!charge.add<SampleType>(geometry.input_ring_elements) ||
212 !charge.add<SampleType>(fft_size)))
213 return std::nullopt;
214
215 geometry.max_synthesis_hop = max_synthesis_hop;
216 geometry.num_bins = static_cast<int>(num_bins);
217 geometry.ring_size = static_cast<int>(ring_size);
218 geometry.retained_bytes = charge.total();
219 return geometry;
220}
221
224template <typename SampleType = float>
226public:
228
235 void prepare(const SpectralFrameEngineConfig& config) {
236 const auto geometry = checked_spectral_frame_engine_geometry<SampleType>(config);
237 assert(geometry.has_value());
238 if (!geometry) return;
239
240 config_ = config;
241 config_.max_synthesis_hop = geometry->max_synthesis_hop;
242 num_bins_ = geometry->num_bins;
243 first_frame_start_ = spectral_frame_engine_first_frame_start(
244 config_.fft_size, config_.analysis_hop, config_.full_overlap_stream_start);
245
246 fft_ = FftT<SampleType>(config_.fft_size);
247 window_ = WindowFunction::generate<SampleType>(config_.fft_size,
248 config_.window);
249
250 // Steady-state OLA window-energy at a fully-overlapped sample for
251 // the analysis hop. Without full-overlap stream start it floors the
252 // per-sample normalization so partial-overlap samples at the stream
253 // start taper to zero instead of being amplified by division by a
254 // near-zero coverage. The floor sits far below any real body coverage
255 // (down to a 4x sparser synthesis hop), so body samples normalize
256 // unchanged.
257 double steady = 0.0;
258 const int n = config_.fft_size, h = config_.analysis_hop;
259 const int center = n; // well inside the plateau
260 for (int j = -n / h - 1; j <= n / h + 1; ++j) {
261 const int idx = center - j * h - (n / 2);
262 if (idx >= 0 && idx < n) {
263 const SampleType w = window_[static_cast<size_t>(idx)];
264 steady += static_cast<double>(w) * w;
265 }
266 }
267 min_norm_ = std::max(static_cast<SampleType>(steady) * SampleType{0.25f},
268 SampleType{1e-9f});
269
270 ring_size_ = geometry->ring_size;
271 ring_mask_ = ring_size_ - 1;
272
273 input_ring_.assign(static_cast<size_t>(geometry->input_ring_elements),
274 SampleType{0.0f});
275 output_ring_.assign(static_cast<size_t>(geometry->output_ring_elements),
276 SampleType{0.0f});
277 norm_ring_.assign(ring_size_, SampleType{0.0f});
278
279 frames_.assign(static_cast<size_t>(geometry->frame_elements),
280 std::complex<SampleType>(SampleType{0.0f}, SampleType{0.0f}));
281 frame_ptrs_.resize(config_.channels);
282 for (int ch = 0; ch < config_.channels; ++ch)
283 frame_ptrs_[ch] = frames_.data() + static_cast<size_t>(ch) * num_bins_;
284
285 time_buf_.assign(config_.fft_size, SampleType{0.0f});
286 guarded_numerator_.assign(
287 config_.guarded_split ? static_cast<size_t>(geometry->input_ring_elements) : 0,
288 SampleType{0});
289 guarded_norm_.assign(config_.guarded_split ? config_.fft_size : 0, SampleType{0});
290
291 reset();
292 }
293
299 int latency_samples() const { return config_.fft_size + config_.analysis_hop; }
300
305 std::int64_t first_frame_start() const {
306 return first_frame_start_;
307 }
308
309 int fft_size() const { return config_.fft_size; }
310 int analysis_hop() const { return config_.analysis_hop; }
311 int num_bins() const { return num_bins_; }
312 int channels() const { return config_.channels; }
313
316 std::uint64_t retained_storage_bytes() const noexcept {
318 std::uint64_t fft_bytes = 0;
320 fft_bytes) ||
321 !charge.add_retained_bytes(fft_bytes) || !charge.add<SampleType>(window_.capacity()) ||
322 !charge.add<SampleType>(input_ring_.capacity()) ||
323 !charge.add<SampleType>(output_ring_.capacity()) ||
324 !charge.add<SampleType>(norm_ring_.capacity()) ||
325 !charge.add<SampleType>(time_buf_.capacity()) ||
326 !charge.add<SampleType>(guarded_numerator_.capacity()) ||
327 !charge.add<SampleType>(guarded_norm_.capacity()) ||
328 !charge.add<std::complex<SampleType>>(frames_.capacity()) ||
329 !charge.add<std::complex<SampleType>*>(frame_ptrs_.capacity()))
330 return std::numeric_limits<std::uint64_t>::max();
331 return charge.total();
332 }
333
336 template <typename Fn>
337 SpectralSplitResult analyze_guarded(const SampleType* const* in, int count, Fn&& fn) {
338 if (!config_.guarded_split || num_bins_ == 0)
340 if (count < 0 || count > config_.max_block || (count != 0 && !in))
342 for (int ch = 0; count != 0 && ch < config_.channels; ++ch)
343 if (!in[ch])
345 constexpr auto limit = std::numeric_limits<std::int64_t>::max();
346 if (samples_fed_ > limit - count || samples_fed_ + count > limit - config_.analysis_hop)
348 analyze(in, count, std::forward<Fn>(fn));
350 }
351
355 synthesize_frame_guarded(const std::complex<SampleType>* const* frames) noexcept {
356 if (!config_.guarded_split || num_bins_ == 0)
358 if (guarded_stopped_)
360 const int n = config_.fft_size;
361 constexpr auto limit = std::numeric_limits<std::int64_t>::max();
362 if (synth_pos_ > limit - n)
364 if (synth_pos_ + n - read_pos_ > ring_size_)
366 if (!frames)
368 for (int ch = 0; ch < config_.channels; ++ch) {
369 if (!frames[ch])
371 for (int bin = 0; bin < num_bins_; ++bin)
372 if (!std::isfinite(frames[ch][bin].real()) ||
373 !std::isfinite(frames[ch][bin].imag()))
375 if (frames[ch][0].imag() != SampleType{0} ||
376 frames[ch][num_bins_ - 1].imag() != SampleType{0})
378 }
379 const int skip = synth_pos_ < 0 ? static_cast<int>(-synth_pos_) : 0;
380 for (int ch = 0; ch < config_.channels; ++ch) {
381 fft_.inverse_real_half(frames[ch], time_buf_.data());
382 for (int i = 0; i < n; ++i)
383 if (!std::isfinite(time_buf_[i]))
385 const auto* ring = output_ring_.data() + static_cast<size_t>(ch) * ring_size_;
386 auto* staged = guarded_numerator_.data() + static_cast<size_t>(ch) * n;
387 for (int i = skip; i < n; ++i) {
388 const auto idx = static_cast<size_t>((synth_pos_ + i) & ring_mask_);
389 const SampleType contribution = time_buf_[i] * window_[i];
390 staged[i] = ring[idx] + contribution;
391 if (!std::isfinite(contribution) || !std::isfinite(staged[i]))
393 }
394 }
395 for (int i = skip; i < n; ++i) {
396 const auto idx = static_cast<size_t>((synth_pos_ + i) & ring_mask_);
397 guarded_norm_[i] = norm_ring_[idx] + window_[i] * window_[i];
398 if (!std::isfinite(guarded_norm_[i]))
400 }
401 for (int ch = 0; ch < config_.channels; ++ch) {
402 auto* ring = output_ring_.data() + static_cast<size_t>(ch) * ring_size_;
403 const auto* staged = guarded_numerator_.data() + static_cast<size_t>(ch) * n;
404 for (int i = skip; i < n; ++i)
405 ring[static_cast<size_t>((synth_pos_ + i) & ring_mask_)] = staged[i];
406 }
407 for (int i = skip; i < n; ++i)
408 norm_ring_[static_cast<size_t>((synth_pos_ + i) & ring_mask_)] = guarded_norm_[i];
409 available_ = std::max(available_, synth_pos_);
410 synth_pos_ += config_.analysis_hop;
412 }
413
417 void stop_synthesis_guarded() noexcept {
418 if (!config_.guarded_split)
419 return;
420 available_ = std::max(available_, synth_pos_);
421 guarded_stopped_ = true;
422 }
423
426 SpectralSplitResult read_output_guarded(SampleType* const* out, int count,
427 int& consumed) noexcept {
428 consumed = 0;
429 if (!config_.guarded_split || num_bins_ == 0)
431 if (count < 0 || count > config_.max_block || (count != 0 && !out))
433 for (int ch = 0; count != 0 && ch < config_.channels; ++ch)
434 if (!out[ch])
436 for (; consumed < count; ++consumed) {
437 if (read_pos_ >= available_)
439 const auto idx = static_cast<size_t>(read_pos_ & ring_mask_);
440 const bool startup = !config_.full_overlap_stream_start && read_pos_ < config_.fft_size;
441 const SampleType norm =
442 startup ? std::max(norm_ring_[idx], min_norm_) : norm_ring_[idx];
443 if (!std::isfinite(norm))
445 for (int ch = 0; ch < config_.channels; ++ch) {
446 const auto* ring = output_ring_.data() + static_cast<size_t>(ch) * ring_size_;
447 auto& value = guarded_numerator_[static_cast<size_t>(ch) * config_.fft_size];
448 value = norm > SampleType{1e-9f} ? ring[idx] / norm : SampleType{0};
449 if (!std::isfinite(value))
451 }
452 for (int ch = 0; ch < config_.channels; ++ch) {
453 out[ch][consumed] = guarded_numerator_[static_cast<size_t>(ch) * config_.fft_size];
454 output_ring_[static_cast<size_t>(ch) * ring_size_ + idx] = SampleType{0};
455 }
456 norm_ring_[idx] = SampleType{0};
457 ++read_pos_;
458 }
460 }
461
467 template <typename Fn>
468 void process(const SampleType* const* in,
469 SampleType* const* out,
470 int num_samples,
471 Fn&& on_frames) {
472 assert(num_samples <= config_.max_block);
473 // Deferred resynthesis. A frame that completes at input sample F is
474 // first read at output sample F + analysis_hop (the latency is
475 // fft_size + analysis_hop), so its inverse transforms and overlap-add
476 // can wait up to a hop. Each channel's runs in the first callback at
477 // or past its own due point inside that hop, spreading what used to
478 // land whole in the callback that completed the frame; whatever is
479 // still pending runs before the next frame (analyze() flushes).
480 // Same arithmetic in the same order: output is bit-identical.
481 run_due_synthesis_(samples_fed_ + num_samples);
482 analyze(in, num_samples, [&](std::complex<SampleType>* const* frames,
483 int bins) {
484 on_frames(frames, bins);
485 if (config_.defer_synthesis)
486 begin_deferred_synthesis_(frames);
487 else
488 synthesize_frame(frames, config_.analysis_hop);
489 });
490 run_due_synthesis_(samples_fed_);
491 // Fixed-latency read: the first latency_samples() outputs are zeros;
492 // afterwards every output pops exactly one final ring sample, so the
493 // input→output delay is constant regardless of block size or phase.
494 const auto lat = static_cast<std::int64_t>(latency_samples());
495 for (int i = 0; i < num_samples; ++i) {
496 if (out_count_ < lat) {
497 for (int ch = 0; ch < config_.channels; ++ch)
498 out[ch][i] = SampleType{0.0f};
499 } else {
500 pop_one(out, i);
501 }
502 ++out_count_;
503 }
504 }
505
513 return static_cast<int>(next_frame_at_ - samples_fed_);
514 }
515
523 template <typename Fn>
524 void analyze(const SampleType* const* in, int num_samples, Fn&& on_frames) {
525 const int n = config_.fft_size;
526 int done = 0;
527 while (done < num_samples) {
528 const auto until_frame = static_cast<int>(
529 std::min<std::int64_t>(next_frame_at_ - samples_fed_,
530 static_cast<std::int64_t>(num_samples - done)));
531 const int run = std::max(until_frame, 1);
532 for (int ch = 0; ch < config_.channels; ++ch) {
533 SampleType* ring = input_ring_.data() + static_cast<size_t>(ch) * n;
534 for (int i = 0; i < run; ++i)
535 ring[(input_pos_ + i) % n] = in[ch][done + i];
536 }
537 input_pos_ = (input_pos_ + run) % n;
538 samples_fed_ += run;
539 done += run;
540
541 if (samples_fed_ == next_frame_at_) {
542 flush_deferred_synthesis_();
543 next_frame_at_ += config_.analysis_hop;
544 emit_frame(on_frames);
545 }
546 }
547 }
548
555 void synthesize_frame(std::complex<SampleType>* const* frames, int synthesis_hop) {
556 assert(synthesis_hop > 0 && synthesis_hop <= config_.max_synthesis_hop);
557 for (int ch = 0; ch < config_.channels; ++ch)
558 synthesize_channel_(frames, ch);
559 finish_synthesis_(synthesis_hop);
560 }
561
564 int available_output() const {
565 return static_cast<int>(available_ - read_pos_);
566 }
567
572 void read_output(SampleType* const* out, int num_samples) {
573 for (int i = 0; i < num_samples; ++i) {
574 if (read_pos_ < available_) {
575 pop_one(out, i);
576 } else {
577 for (int ch = 0; ch < config_.channels; ++ch)
578 out[ch][i] = SampleType{0.0f};
579 }
580 }
581 }
582
583 void reset() {
584 std::fill(input_ring_.begin(), input_ring_.end(), SampleType{0.0f});
585 std::fill(output_ring_.begin(), output_ring_.end(), SampleType{0.0f});
586 std::fill(norm_ring_.begin(), norm_ring_.end(), SampleType{0.0f});
587 std::fill(frames_.begin(), frames_.end(),
588 std::complex<SampleType>(SampleType{0.0f}, SampleType{0.0f}));
589 input_pos_ = 0;
590 samples_fed_ = 0;
591 next_frame_at_ = config_.fft_size + first_frame_start_;
592 synth_pos_ = first_frame_start_;
593 available_ = 0;
594 read_pos_ = 0;
595 out_count_ = 0;
596 deferred_frames_ = nullptr;
597 deferred_next_channel_ = 0;
598 guarded_stopped_ = false;
599 }
600
601private:
602 // One channel of synthesize_frame(): inverse transform and overlap-add.
603 // Pre-stream samples of a frame that starts before position 0 are never
604 // read, so they are never written: the ring slots they would alias stay
605 // clean for the real stream.
606 void synthesize_channel_(std::complex<SampleType>* const* frames, int ch) {
607 const int n = config_.fft_size;
608 const int skip =
609 synth_pos_ < 0 ? static_cast<int>(std::min<std::int64_t>(-synth_pos_, n)) : 0;
610 // The frame holds DC..Nyquist of a real signal; the real inverse reads
611 // exactly that and ignores DC/Nyquist imaginary parts, which a full
612 // complex inverse would only have sent to the discarded imaginary output.
613 fft_.inverse_real_half(frames[ch], time_buf_.data());
614 SampleType* ring = output_ring_.data() + static_cast<size_t>(ch) * ring_size_;
615 for (int i = skip; i < n; ++i) {
616 const auto idx = static_cast<size_t>((synth_pos_ + i) & ring_mask_);
617 ring[idx] += time_buf_[static_cast<size_t>(i)] * window_[static_cast<size_t>(i)];
618 }
619 }
620
621 // The rest of synthesize_frame(): the shared normalization ring, then
622 // advance the synthesis position.
623 void finish_synthesis_(int synthesis_hop) {
624 const int n = config_.fft_size;
625 const int skip =
626 synth_pos_ < 0 ? static_cast<int>(std::min<std::int64_t>(-synth_pos_, n)) : 0;
627 for (int i = skip; i < n; ++i) {
628 const auto idx = static_cast<size_t>((synth_pos_ + i) & ring_mask_);
629 norm_ring_[idx] += window_[static_cast<size_t>(i)] * window_[static_cast<size_t>(i)];
630 }
631 synth_pos_ += synthesis_hop;
632 // Samples before the start of the frame just written are final.
633 available_ = std::max(available_, synth_pos_ - synthesis_hop);
634 }
635
636 void begin_deferred_synthesis_(std::complex<SampleType>* const* frames) {
637 deferred_frames_ = frames;
638 deferred_next_channel_ = 0;
639 deferred_frame_at_ = samples_fed_;
640 }
641
642 // Run every deferred channel whose due point is before `fed`.
643 void run_due_synthesis_(std::int64_t fed) {
644 const int channels = config_.channels;
645 while (deferred_frames_ != nullptr) {
646 const std::int64_t due =
647 deferred_frame_at_ + static_cast<std::int64_t>(config_.analysis_hop) *
648 (deferred_next_channel_ + 1) / (channels + 1);
649 if (due > fed)
650 return;
651 step_deferred_synthesis_();
652 }
653 }
654
655 void step_deferred_synthesis_() {
656 synthesize_channel_(deferred_frames_, deferred_next_channel_);
657 if (++deferred_next_channel_ == config_.channels) {
658 finish_synthesis_(config_.analysis_hop);
659 deferred_frames_ = nullptr;
660 }
661 }
662
663 void flush_deferred_synthesis_() {
664 while (deferred_frames_ != nullptr)
665 step_deferred_synthesis_();
666 }
667
668 // Pop one final sample (all channels) into out[..][i], normalizing by
669 // the accumulated squared synthesis window and clearing the slot.
670 void pop_one(SampleType* const* out, int i) {
671 // A frame whose synthesis is deferred is final for its first hop
672 // before available_ (conservative by a hop) says so.
673 assert(read_pos_ < available_ + config_.analysis_hop);
674 const auto idx = static_cast<size_t>(read_pos_ & ring_mask_);
675 // Floor only the stream-start partial-overlap region, which exists
676 // only without full-overlap stream start. Elsewhere per-sample
677 // normalization must remain exact for non-COLA windows/hops whose
678 // valid body coverage can dip below the startup floor; a genuinely
679 // zero coverage is still guarded below.
680 const bool startup_edge =
681 !config_.full_overlap_stream_start && read_pos_ < config_.fft_size;
682 const SampleType norm = startup_edge
683 ? std::max(norm_ring_[idx], min_norm_)
684 : norm_ring_[idx];
685 for (int ch = 0; ch < config_.channels; ++ch) {
686 SampleType* ring =
687 output_ring_.data() + static_cast<size_t>(ch) * ring_size_;
688 out[ch][i] =
689 norm > SampleType{1e-9f} ? ring[idx] / norm : SampleType{0.0f};
690 ring[idx] = SampleType{0.0f};
691 }
692 norm_ring_[idx] = SampleType{0.0f};
693 ++read_pos_;
694 }
695
696 // Window + transform the last fft_size samples of every channel's
697 // input ring and hand the frame group to the callback.
698 template <typename Fn>
699 void emit_frame(Fn&& on_frames) {
700 const int n = config_.fft_size;
701 for (int ch = 0; ch < config_.channels; ++ch) {
702 const SampleType* ring =
703 input_ring_.data() + static_cast<size_t>(ch) * n;
704 for (int i = 0; i < n; ++i)
705 time_buf_[static_cast<size_t>(i)] =
706 ring[(input_pos_ + i) % n] * window_[static_cast<size_t>(i)];
707 fft_.forward_real_half(time_buf_.data(),
708 frames_.data() + static_cast<size_t>(ch) * num_bins_);
709 }
710 on_frames(frame_ptrs_.data(), num_bins_);
711 }
712
713 SpectralFrameEngineConfig config_;
714 FftT<SampleType> fft_{2048};
715 std::vector<SampleType> window_;
716 int num_bins_ = 0;
717 int ring_size_ = 0;
718 int ring_mask_ = 0;
719 SampleType min_norm_ = SampleType{1e-9f}; // OLA coverage floor for edge taper.
720 std::int64_t first_frame_start_ = 0; // <= 0; see first_frame_start().
721
722 std::vector<SampleType> input_ring_; // channels * fft_size
723 std::vector<SampleType> output_ring_; // channels * ring_size
724 std::vector<SampleType> norm_ring_; // ring_size (shared across channels)
725
726 std::vector<std::complex<SampleType>> frames_; // channels * num_bins
727 std::vector<std::complex<SampleType>*> frame_ptrs_; // channels
728 std::vector<SampleType> time_buf_; // fft_size
729 std::vector<SampleType> guarded_numerator_; // channels * fft_size
730 std::vector<SampleType> guarded_norm_; // fft_size
731 bool guarded_stopped_ = false;
732
733 int input_pos_ = 0;
734 std::int64_t samples_fed_ = 0;
735 std::int64_t next_frame_at_ = 0;
736 std::int64_t synth_pos_ = 0; // next frame start (absolute samples)
737 std::int64_t available_ = 0; // final samples high-water mark
738 std::int64_t read_pos_ = 0; // absolute read position
739 std::int64_t out_count_ = 0; // samples emitted by process()
740 // process()'s deferred resynthesis: the frame group awaiting it (the
741 // engine's own frame buffers, untouched until the next frame), the next
742 // channel to synthesize, and the input sample the frame completed at.
743 std::complex<SampleType>* const* deferred_frames_ = nullptr;
744 int deferred_next_channel_ = 0;
745 std::int64_t deferred_frame_at_ = 0;
746};
747
750
751} // 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
Definition fft.hpp:176
void inverse_real_half(const std::complex< SampleType > *input, SampleType *output, SampleType scale) const
Inverse of forward_real_half().
Definition fft.hpp:469
Streaming multichannel STFT/WOLA engine.
Definition spectral_frame_engine.hpp:225
int latency_samples() const
Fixed delay of the equal-hop process() path.
Definition spectral_frame_engine.hpp:299
void process(const SampleType *const *in, SampleType *const *out, int num_samples, Fn &&on_frames)
Equal-hop streaming: push num_samples per channel, invoke on_frames(std::complex<SampleType>* const* ...
Definition spectral_frame_engine.hpp:468
std::int64_t first_frame_start() const
Stream position of the first analysis frame after prepare()/reset(): 0 without full-overlap stream st...
Definition spectral_frame_engine.hpp:305
SpectralSplitResult synthesize_frame_guarded(const std::complex< SampleType > *const *frames) noexcept
Equal-hop split synthesis.
Definition spectral_frame_engine.hpp:355
int num_bins() const
Definition spectral_frame_engine.hpp:311
void stop_synthesis_guarded() noexcept
After a fatal frame at the current origin, prior samples are final: this frame cannot contribute befo...
Definition spectral_frame_engine.hpp:417
void prepare(const SpectralFrameEngineConfig &config)
RT contract: prepare() allocates FFT/window/ring/frame storage and is not audio-thread safe.
Definition spectral_frame_engine.hpp:235
int fft_size() const
Definition spectral_frame_engine.hpp:309
void analyze(const SampleType *const *in, int num_samples, Fn &&on_frames)
Split API — analysis only.
Definition spectral_frame_engine.hpp:524
SpectralSplitResult read_output_guarded(SampleType *const *out, int count, int &consumed) noexcept
Pop a finite prefix and return its length.
Definition spectral_frame_engine.hpp:426
int available_output() const
Split API — number of final (fully overlapped) output samples that can be read right now.
Definition spectral_frame_engine.hpp:564
void reset()
Definition spectral_frame_engine.hpp:583
std::uint64_t retained_storage_bytes() const noexcept
Logical retained allocations, using actual container capacities and the selected FFT provider's backi...
Definition spectral_frame_engine.hpp:316
void synthesize_frame(std::complex< SampleType > *const *frames, int synthesis_hop)
Split API — overlap-add one spectral frame (all channels) at the current synthesis position,...
Definition spectral_frame_engine.hpp:555
int channels() const
Definition spectral_frame_engine.hpp:312
int analysis_hop() const
Definition spectral_frame_engine.hpp:310
SpectralSplitResult analyze_guarded(const SampleType *const *in, int count, Fn &&fn)
Checked split analysis refuses the whole block before ingest on a clock or block-limit error.
Definition spectral_frame_engine.hpp:337
int samples_until_next_frame() const
Samples that must still be fed before the next analysis frame completes (>= 1).
Definition spectral_frame_engine.hpp:512
void read_output(SampleType *const *out, int num_samples)
Split API — pop exactly num_samples per channel into out.
Definition spectral_frame_engine.hpp:572
static std::vector< SampleType > generate(int size, Type type, SampleType param=SampleType{0.0f})
Definition windowing.hpp:56
Type
Definition windowing.hpp:43
@ hann
Definition windowing.hpp:45
@ ring
Definition string.hpp:18
Definition additive_bank.hpp:218
@ NotPrepared
Definition source_filter_analysis.hpp:30
@ Ok
Definition source_filter_analysis.hpp:29
bool checked_fft_retained_bytes(std::uint64_t fft_size, std::uint64_t target_max_bytes, std::uint64_t &bytes) noexcept
Complete retained-storage contract for one prepared FftT.
Definition fft.hpp:133
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
@ center
Definition multi_channel_meter.hpp:68
bool is_valid_spectral_frame_geometry(int fft_size, int analysis_hop) noexcept
Definition spectral_frame_engine.hpp:108
SpectralFrameEngineT< double > SpectralFrameEngine64
Definition spectral_frame_engine.hpp:749
SpectralFrameEngineT< float > SpectralFrameEngine
Definition spectral_frame_engine.hpp:748
SpectralSplitResult
Definition spectral_frame_engine.hpp:81
@ StreamStopped
Definition spectral_frame_engine.hpp:90
@ OutputUnderrun
Definition spectral_frame_engine.hpp:89
@ InvalidFrame
Definition spectral_frame_engine.hpp:85
@ NotPrepared
Definition spectral_frame_engine.hpp:83
@ Ok
Definition spectral_frame_engine.hpp:82
@ ChronologyExhausted
Definition spectral_frame_engine.hpp:86
@ NonfiniteSynthesis
Definition spectral_frame_engine.hpp:88
@ InvalidBlock
Definition spectral_frame_engine.hpp:84
@ CapacityExceeded
Definition spectral_frame_engine.hpp:87
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
bool checked_capacity_sum(std::uint64_t lhs, std::uint64_t rhs, std::uint64_t maximum, std::uint64_t &sum) noexcept
Definition checked_allocation.hpp:21
constexpr std::uint64_t kTargetAddressMaximumBytes
Definition checked_allocation.hpp:9
@ i
Definition formant_filter_bank.hpp:20
constexpr int kSpectralFrameEngineMinimumFftSize
Definition spectral_frame_engine.hpp:93
constexpr std::int64_t spectral_frame_engine_first_frame_start(int fft_size, int analysis_hop, bool full_overlap_stream_start) noexcept
Stream position of the first analysis frame after prepare()/reset().
Definition spectral_frame_engine.hpp:120
double spectral_ola_window_energy(const SampleType *window, int fft_size, int hop, std::int64_t first_frame_start, std::int64_t position) noexcept
Accumulated squared synthesis window at stream position for equal-hop frames of length fft_size start...
Definition spectral_frame_engine.hpp:132
constexpr std::uint64_t kSpectralFrameEngineMaximumRingSize
Definition spectral_frame_engine.hpp:95
@ n
Definition input_events.hpp:40
Configuration for a SpectralFrameEngine.
Definition spectral_frame_engine.hpp:56
bool guarded_split
Prepare transactional inverse/OLA scratch for the guarded split API.
Definition spectral_frame_engine.hpp:78
int channels
Definition spectral_frame_engine.hpp:59
int max_block
Definition spectral_frame_engine.hpp:60
WindowFunction::Type window
Definition spectral_frame_engine.hpp:62
int fft_size
Definition spectral_frame_engine.hpp:57
int analysis_hop
Definition spectral_frame_engine.hpp:58
int max_synthesis_hop
Definition spectral_frame_engine.hpp:61
bool full_overlap_stream_start
true: analysis frames start before the stream so the first sample after prepare()/reset() gets full o...
Definition spectral_frame_engine.hpp:70
bool defer_synthesis
process() only: run each channel's inverse transform and overlap-add at its own point in the hop afte...
Definition spectral_frame_engine.hpp:75
Definition spectral_frame_engine.hpp:98
std::uint64_t frame_elements
Definition spectral_frame_engine.hpp:104
int ring_size
Definition spectral_frame_engine.hpp:101
std::uint64_t output_ring_elements
Definition spectral_frame_engine.hpp:103
std::uint64_t retained_bytes
Definition spectral_frame_engine.hpp:105
int num_bins
Definition spectral_frame_engine.hpp:100
int max_synthesis_hop
Definition spectral_frame_engine.hpp:99
std::uint64_t input_ring_elements
Definition spectral_frame_engine.hpp:102