Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
realtime_pitch_time_processor.hpp
Go to the documentation of this file.
1#pragma once
2
36
37#include <pulp/signal/detail/fractional_synthesis_hop_accumulator.hpp>
48#include <algorithm>
49#include <cassert>
50#include <cmath>
51#include <complex>
52#include <cstdint>
53#include <limits>
54#include <vector>
55
56namespace pulp::signal {
57
58template <typename SampleType = float>
60public:
62
71 double sample_rate,
72 const RealtimePitchTimeConfig& config,
73 std::uint64_t target_max_bytes = kTargetAddressMaximumBytes) {
74 if (!std::isfinite(sample_rate) || !(sample_rate > 0.0))
76 if (config.channels < 1 || config.channels > kRealtimePitchTimeMaximumChannels)
78 if (config.max_block <= 0) return PitchTimePrepareStatus::invalid_max_block;
79 if (config.mode == PitchTimeMode::time_stretch
80 && (!std::isfinite(config.max_time_ratio) || config.max_time_ratio < 1.0f))
82 const double max_pitch_ratio =
83 std::exp2(static_cast<double>(config.max_pitch_semitones) / 12.0);
84 if (!std::isfinite(config.max_pitch_semitones) || config.max_pitch_semitones < 0.0f
85 || !std::isfinite(max_pitch_ratio))
87
89 const auto geometry_status = checked_realtime_pitch_time_prepared_geometry(
90 config, max_pitch_ratio, target_max_bytes, geometry);
91 if (geometry_status != PitchTimePrepareStatus::prepared) return geometry_status;
92 const bool quality = config.quality == PitchTimeQuality::quality;
93
95 env_config.fft_size = geometry.fft_size;
96 env_config.true_envelope_iterations = config.true_envelope_iterations;
98 if (prepared_envelope.prepare(env_config) != SourceFilterAnalysisStatus::Ok)
100
101 config_ = config;
102 sample_rate_ = sample_rate;
103 fft_size_ = geometry.fft_size;
104 analysis_hop_ = geometry.analysis_hop;
105 maximum_stream_output_lag_samples_ = geometry.maximum_stream_output_lag_samples;
106 engine_.prepare(geometry.engine_config);
107
108 coordinator_.prepare(fft_size_, config.channels);
109
110 typename TransientPhasePolicyT<SampleType>::Config transient_config;
111 transient_config.fft_size = fft_size_;
112 if (config.transient_sensitivity > 0.0f)
113 transient_config.sensitivity = config.transient_sensitivity;
114 transient_.prepare(transient_config);
115
116 typename FreezeHoldT<SampleType>::Config freeze_config;
117 freeze_config.fft_size = fft_size_;
118 freeze_config.channels = config.channels;
119 freeze_config.analysis_hop = analysis_hop_;
120 freeze_.prepare(freeze_config);
121
122 envelope_ = std::move(prepared_envelope);
123
126 smoother_config.attack_seconds = config.pitch_smoothing_seconds;
127 smoother_config.release_seconds = config.pitch_smoothing_seconds;
128 pitch_smoother_.prepare(sample_rate, smoother_config);
129 formant_smoother_.prepare(sample_rate, smoother_config);
130
131 // Stretched-stream ring: must span the read-to-write gap
132 // (latency * ratio) plus one engine drain burst and one block.
133 ring_size_ = geometry.ring_size;
134 ring_mask_ = ring_size_ - 1;
135 stretch_ring_.assign(static_cast<std::size_t>(geometry.stretch_ring_elements),
136 SampleType{0});
137
138 drain_buf_.assign(static_cast<std::size_t>(geometry.drain_elements), SampleType{0});
139 drain_ptrs_.resize(static_cast<size_t>(config.channels));
140 finalize_zero_buf_.assign(static_cast<std::size_t>(geometry.finalize_zero_elements),
141 SampleType{0});
142 finalize_zero_ptrs_.resize(static_cast<size_t>(config.channels));
143 for (int ch = 0; ch < config.channels; ++ch)
144 finalize_zero_ptrs_[static_cast<size_t>(ch)] =
145 finalize_zero_buf_.data()
146 + static_cast<size_t>(ch)
147 * static_cast<size_t>(geometry.engine_config.max_block);
148 max_synthesis_hop_ = geometry.engine_config.max_synthesis_hop;
149
150 // Noise-morphing front end: STN decomposition over the spectrum
151 // plus one NoiseMorpher per channel. The STN mask is computed from
152 // channel 0 and shared across channels for a coherent split.
153 const int spectral_bins = fft_size_ / 2 + 1;
154 if (config.noise_morphing) {
155 StnConfig stn_config;
156 stn_config.num_bins = spectral_bins;
157 stn_config.time_median = quality ? 7 : 5;
158 stn_config.freq_median = quality ? 11 : 7;
159 // The morph split applies the mask to the frame it just pushed, so
160 // align the mask to the newest frame. Centered masks lag by
161 // (time_median-1)/2 frames and misroute transient-onset energy into
162 // the noise path (decohering + level-losing percussive hits).
163 stn_config.causal = true;
164 stn_.prepare(stn_config);
165 noise_morphers_.resize(static_cast<size_t>(config.channels));
166 // Same seed across channels → coherent (mono-safe) noise phase:
167 // identical input yields identical output and the noise sums
168 // correctly to mono. The per-channel magnitude envelopes still
169 // give each channel its own color.
170 for (int ch = 0; ch < config.channels; ++ch)
171 noise_morphers_[static_cast<size_t>(ch)].prepare(spectral_bins);
172 mag_scratch_.assign(static_cast<size_t>(spectral_bins), SampleType{0});
173 noise_env_.assign(static_cast<size_t>(config.channels) * spectral_bins, SampleType{0});
174 noise_spec_.assign(static_cast<size_t>(spectral_bins), std::complex<SampleType>{});
175 }
176 if (config.sinc_resampling) {
177 resampler_.build();
178 tap_scratch_.assign(static_cast<size_t>(resampler_.taps()), SampleType{0});
179 }
180 reset();
182 }
183
185 int latency_samples() const { return fft_size_ + analysis_hop_ + kReadGuard; }
186 int fft_size() const { return fft_size_; }
187
188 void set_pitch_semitones(float semitones) {
189 pitch_smoother_.set_target(
190 std::clamp(semitones, -config_.max_pitch_semitones, config_.max_pitch_semitones));
191 }
192
193 void set_formant_semitones(float semitones) {
194 formant_smoother_.set_target(
195 std::clamp(semitones, -config_.max_pitch_semitones, config_.max_pitch_semitones));
196 }
197
198 void set_formant_mode(FormantMode mode) { config_.formant_mode = mode; }
199
203 void set_frozen(bool frozen) { freeze_.set_frozen(frozen); }
204 bool is_frozen() const { return freeze_.is_latched(); }
205
207 void set_time_ratio(float ratio) {
208 time_ratio_ = static_cast<SampleType>(
209 std::clamp(ratio, 1.0f / config_.max_time_ratio, config_.max_time_ratio));
210 }
211
213 double achieved_time_ratio() const {
214 return frames_done_ > 0
215 ? static_cast<double>(synth_accum_int_)
216 / (static_cast<double>(frames_done_) * analysis_hop_)
217 : static_cast<double>(time_ratio_);
218 }
219
221 void process(const SampleType* const* in, SampleType* const* out, int num_samples) {
222 assert(config_.mode == PitchTimeMode::realtime_pitch);
223 assert(num_samples <= config_.max_block);
224
225 feed_engine(in, num_samples);
226 pitch_smoother_.advance(num_samples);
227 formant_smoother_.advance(num_samples);
228
229 // Catmull-Rom read of the stretched stream, positioned through
230 // the producer's own frame map: frame f covers input starting at
231 // f * hop and was synthesized starting at stretched position
232 // S_f, so the stretched position playing input time t is the
233 // piecewise-linear interpolation through the (f * hop -> S_f)
234 // pairs. Output sample T plays input time T - latency exactly —
235 // no open-loop ratio integration, hence no drift against the hop
236 // accumulator, exact latency at every ratio, and the local read
237 // slope (the pitch ratio) is consistent with the synthesis hops
238 // by construction. The reader trails the producer by under two
239 // frames, which the latency guard plus drain cadence always
240 // covers in realtime use.
241 const auto lat = static_cast<std::int64_t>(latency_samples());
242 const auto hop = static_cast<std::int64_t>(analysis_hop_);
243 for (int i = 0; i < num_samples; ++i) {
244 if (out_count_ < lat) {
245 for (int ch = 0; ch < config_.channels; ++ch) out[ch][i] = SampleType{0};
246 } else {
247 const std::int64_t target_in = out_count_ - lat;
248 const std::int64_t f = target_in / hop;
249 if (f + 1 < frames_done_) {
250 const std::int64_t s0 = frame_starts_[static_cast<size_t>(f & kFrameMapMask)];
251 const std::int64_t s1 =
252 frame_starts_[static_cast<size_t>((f + 1) & kFrameMapMask)];
253 const double frac =
254 static_cast<double>(target_in - f * hop) / static_cast<double>(hop);
255 read_pos_ = static_cast<double>(s0)
256 + frac * static_cast<double>(s1 - s0);
257 read_fractional(out, i);
258 } else {
259 for (int ch = 0; ch < config_.channels; ++ch) out[ch][i] = SampleType{0};
260 }
261 }
262 ++out_count_;
263 }
264 }
265
269 PitchTimeStreamFeedStatus feed(const SampleType* const* in, int num_samples) {
270 if (config_.mode != PitchTimeMode::time_stretch || in == nullptr || num_samples < 0
271 || num_samples > config_.max_block)
273 if (input_closed_) return PitchTimeStreamFeedStatus::input_closed;
274 if (num_samples == 0) return PitchTimeStreamFeedStatus::accepted;
275 if (!can_accept_input(num_samples)) return PitchTimeStreamFeedStatus::backpressure;
276 feed_engine(in, num_samples);
277 pitch_smoother_.advance(num_samples);
278 formant_smoother_.advance(num_samples);
280 }
281
284 return static_cast<int>(stretch_written_ - stretch_read_);
285 }
286
289 int output_free_space() const {
290 if (config_.mode != PitchTimeMode::time_stretch) return 0;
291 return static_cast<int>(static_cast<std::int64_t>(ring_size_)
292 - (stretch_written_ - stretch_read_));
293 }
294
298 int input_priming_samples() const { return fft_size_ + analysis_hop_; }
299 int output_alignment_samples() const { return 0; }
304 return maximum_stream_output_lag_samples_;
305 }
306
312 return config_.mode == PitchTimeMode::time_stretch && ring_size_ > 0
313 ? engine_.samples_until_next_frame()
314 : 0;
315 }
316
323 return finalize(config_.max_block);
324 }
325
330 PitchTimeStreamFinalizePlan plan_finalize(int max_samples) const noexcept {
331 if (config_.mode != PitchTimeMode::time_stretch)
333 if (max_samples <= 0 || max_samples > config_.max_block)
335 if (final_output_limit_ >= 0 && stretch_written_ >= final_output_limit_) {
338 0};
339 }
340 const int padding_remaining = input_closed_
341 ? finalize_padding_remaining_
342 : fft_size_ + 2 * analysis_hop_;
343 const int run = std::min(max_samples, padding_remaining);
344 if (run <= 0 || !can_accept_input(run))
347 }
348
353 if (config_.mode != PitchTimeMode::time_stretch)
355 if (max_samples <= 0 || max_samples > config_.max_block)
357 if (!input_closed_) {
358 input_closed_ = true;
359 final_input_count_ = input_count_;
360 finalize_padding_remaining_ = fft_size_ + 2 * analysis_hop_;
361 }
362 if (final_output_limit_ >= 0 && stretch_written_ >= final_output_limit_)
365
366 const int run = std::min(max_samples, finalize_padding_remaining_);
368 if (!can_accept_input(run)) return PitchTimeStreamFinalizeStatus::backpressure;
369
370 feed_engine(finalize_zero_ptrs_.data(), run);
371 pitch_smoother_.advance(run);
372 formant_smoother_.advance(run);
373 finalize_padding_remaining_ -= run;
374
375 if (final_output_limit_ >= 0 && stretch_written_ >= final_output_limit_)
379 }
380
384 int read_stretched(SampleType* const* out, int num_samples) {
385 if (out == nullptr || num_samples <= 0) return 0;
386 int read = 0;
387 for (int i = 0; i < num_samples; ++i) {
388 const bool valid = stretch_read_ < stretch_written_;
389 const auto idx = static_cast<size_t>(stretch_read_ & ring_mask_);
390 for (int ch = 0; ch < config_.channels; ++ch)
391 out[ch][i] = valid
392 ? stretch_ring_[static_cast<size_t>(ch) * ring_size_ + idx]
393 : SampleType{0};
394 if (valid) {
395 ++stretch_read_;
396 ++read;
397 }
398 }
399 return read;
400 }
401
402 void reset() {
403 engine_.reset();
404 coordinator_.reset();
405 transient_.reset();
406 freeze_.reset();
407 if (config_.noise_morphing) {
408 stn_.reset();
409 for (auto& m : noise_morphers_) m.reset();
410 }
411 std::fill(stretch_ring_.begin(), stretch_ring_.end(), SampleType{0});
412 pitch_smoother_.set_immediate(pitch_smoother_.target());
413 formant_smoother_.set_immediate(formant_smoother_.target());
414 synth_hop_accumulator_.reset();
415 synth_accum_int_ = 0;
416 frames_done_ = 0;
417 stretch_written_ = 0;
418 stretch_read_ = 0;
419 read_pos_ = 0.0;
420 std::fill(std::begin(frame_starts_), std::end(frame_starts_),
421 static_cast<std::int64_t>(0));
422 out_count_ = 0;
423 input_count_ = 0;
424 input_closed_ = false;
425 final_input_count_ = 0;
426 final_output_limit_ = -1;
427 finalize_padding_remaining_ = 0;
428 }
429
430private:
431 // Push input through analysis; per frame: phase-propagate, apply the
432 // formant correction, synthesize at the accumulated hop, and drain
433 // the engine's final samples into the stretched ring.
434 void feed_engine(const SampleType* const* in, int num_samples) {
435 int done = 0;
436 while (done < num_samples) {
437 // Chunk exactly to the next analysis-frame boundary so offset_in_block_
438 // is the true in-block offset at which the frame completes. handle_frame
439 // evaluates the smoothed pitch/formant ratio at offset_in_block_, so a
440 // boundary-aligned chunk makes that per-frame control offset exact.
441 const int until = std::max(1, engine_.samples_until_next_frame());
442 const int run = std::min(num_samples - done, until);
443 offset_in_block_ = done + run;
444 engine_.analyze(advance_ptrs(in, done), run,
445 [this](std::complex<SampleType>* const* frames, int bins) {
446 handle_frame(frames, bins);
447 });
448 done += run;
449 input_count_ += run;
450 }
451 }
452
453 void handle_frame(std::complex<SampleType>* const* frames, int bins) {
454 // Controls evaluated at the frame boundary. The smoothers still
455 // sit at the block start here (they advance after feeding), so
456 // the frame's in-block position is a positive forward offset.
457 const SampleType pitch_ratio =
458 config_.mode == PitchTimeMode::realtime_pitch
459 ? static_cast<SampleType>(pitch_smoother_.ratio_at(offset_in_block_))
460 : SampleType{1};
461 const SampleType formant_ratio =
462 static_cast<SampleType>(formant_smoother_.ratio_at(offset_in_block_));
463 const SampleType stretch = config_.mode == PitchTimeMode::time_stretch
464 ? time_ratio_
465 : pitch_ratio;
466
467 // Integer synthesis hop from the fractional accumulator, so the
468 // average hop tracks stretch * analysis_hop exactly.
469 const int hop = synth_hop_accumulator_.advance(
470 static_cast<double>(stretch) * static_cast<double>(analysis_hop_),
471 1, max_synthesis_hop_);
472 // Record this frame's stretched start position for the reader's
473 // input-time -> stretched-position map.
474 frame_starts_[static_cast<size_t>(frames_done_ & kFrameMapMask)] = synth_accum_int_;
475 synth_accum_int_ += hop;
476 ++frames_done_;
477 update_final_output_limit();
478
479 // Freeze first: held frames look like live steady-state input, so
480 // pitch/formant control downstream keeps working over the hold.
481 freeze_.process_group(frames, config_.channels, bins);
482
483 // Morphing only helps when time/pitch scaling is actually engaged;
484 // at unity the baseline path is exactly transparent, so bypass the
485 // split (which would otherwise re-randomise the phase of low-energy
486 // bins and shallow the null).
487 const bool morph =
488 config_.noise_morphing
489 && std::abs(static_cast<double>(stretch - SampleType{1})) > 1e-4;
490
491 // Noise-morphing split: pull the noise component out of the vocoder
492 // path so phase propagation only sees the sines+transients (which it
493 // handles well); the noise is regenerated separately and added back
494 // after the tonal processing (so the coordinator never re-locks its
495 // random phase). The STN mask is computed from channel 0 and shared,
496 // giving a coherent split; per-channel morphers decorrelate the
497 // stereo noise. The mask carries the StnDecomposer's small inherent
498 // delay, applied to the current frame — inaudible for stationary
499 // noise.
500 if (morph) {
501 for (int k = 0; k < bins; ++k)
502 mag_scratch_[static_cast<size_t>(k)] = std::abs(frames[0][k]);
503 const StnMasksT<SampleType>& masks = stn_.process(mag_scratch_.data());
504 for (int ch = 0; ch < config_.channels; ++ch) {
505 std::complex<SampleType>* f = frames[ch];
506 SampleType* env = noise_env_.data() + static_cast<size_t>(ch) * bins;
507 for (int k = 0; k < bins; ++k) {
508 const SampleType nm = masks.noise[static_cast<size_t>(k)];
509 env[k] = std::abs(f[k]) * nm;
510 f[k] *= (SampleType{1} - nm);
511 }
512 }
513 }
514
515 const SampleType reset_amount =
516 config_.transient_preservation
517 ? transient_.analyze(frames, config_.channels, bins)
518 : SampleType{0};
519 coordinator_.process_group(frames, bins, analysis_hop_, hop, reset_amount);
520
521 // Formant path: warp = pitch_ratio / formant_ratio in preserve
522 // mode (cancels the resampler's envelope scaling), 1 / formant_ratio
523 // in follow mode. warp == 1 is an exact bypass inside the shifter.
524 const SampleType warp =
525 (config_.formant_mode == FormantMode::preserve ? pitch_ratio : SampleType{1})
526 / formant_ratio;
527 envelope_.process_group(frames, config_.channels, bins, warp);
528
529 // Add the morphed noise back to the (now phase-propagated) tonal
530 // frame: each channel regenerates its noise from the captured
531 // envelope with fresh random phase, so successive synthesis frames
532 // are decorrelated and overlap-add to natural noise of the right
533 // color at any stretch ratio.
534 if (morph) {
535 // Random-phase noise frames overlap-add INCOHERENTLY while the WOLA
536 // normalizes for COHERENT summation, so morphed noise renders ~4-5 dB
537 // too quiet (an energy-conservation bug: measured -4 to -8 LUFS total
538 // signal). Compensate by the Hann random-phase factor sqrt(8/3) ≈ 1.63
539 // (Hann mean-square), with a gentle overlap correction since denser
540 // overlap (time compression) loses a little more.
541 const SampleType overlap = static_cast<SampleType>(fft_size_)
542 / static_cast<SampleType>(hop);
543 const SampleType noise_gain = std::sqrt(static_cast<SampleType>(8.0 / 3.0))
544 * std::pow(overlap / static_cast<SampleType>(8.0),
545 static_cast<SampleType>(0.08));
546 for (int ch = 0; ch < config_.channels; ++ch) {
547 std::complex<SampleType>* f = frames[ch];
548 SampleType* env = noise_env_.data() + static_cast<size_t>(ch) * bins;
549 NoiseMorpherT<SampleType>& m = noise_morphers_[static_cast<size_t>(ch)];
550 m.set_synthesis_gain(noise_gain);
551 m.push_envelope(env);
552 m.synthesize(SampleType{1}, noise_spec_.data());
553 for (int k = 0; k < bins; ++k) f[k] += noise_spec_[static_cast<size_t>(k)];
554 }
555 }
556
557 engine_.synthesize_frame(frames, hop);
558 drain_engine();
559 }
560
561 void drain_engine() {
562 int avail = engine_.available_output();
563 while (avail > 0) {
564 const int chunk = std::min(avail, static_cast<int>(
565 drain_buf_.size() / static_cast<size_t>(config_.channels)));
566 for (int ch = 0; ch < config_.channels; ++ch)
567 drain_ptrs_[static_cast<size_t>(ch)] =
568 drain_buf_.data() + static_cast<size_t>(ch)
569 * (drain_buf_.size() / static_cast<size_t>(config_.channels));
570 engine_.read_output(drain_ptrs_.data(), chunk);
571 int publish = chunk;
572 if (config_.mode == PitchTimeMode::time_stretch && final_output_limit_ >= 0)
573 publish = static_cast<int>(std::min<std::int64_t>(
574 publish, std::max<std::int64_t>(0, final_output_limit_ - stretch_written_)));
575 if (config_.mode == PitchTimeMode::time_stretch)
576 assert(publish <= output_free_space());
577 for (int ch = 0; ch < config_.channels; ++ch) {
578 const SampleType* src = drain_ptrs_[static_cast<size_t>(ch)];
579 SampleType* ring = stretch_ring_.data() + static_cast<size_t>(ch) * ring_size_;
580 for (int i = 0; i < publish; ++i)
581 ring[static_cast<size_t>((stretch_written_ + i) & ring_mask_)] = src[i];
582 }
583 stretch_written_ += publish;
584 avail -= chunk;
585 }
586 }
587
588 bool can_accept_input(int num_samples) const {
589 const auto frames = static_cast<std::int64_t>(num_samples / analysis_hop_ + 2);
590 const auto worst_output = frames * static_cast<std::int64_t>(max_synthesis_hop_);
591 return worst_output <= output_free_space();
592 }
593
594 void update_final_output_limit() {
595 if (!input_closed_ || final_output_limit_ >= 0) return;
596 const auto hop = static_cast<std::int64_t>(analysis_hop_);
597 const auto frame = final_input_count_ / hop;
598 if (frames_done_ <= frame + 1) return;
599 const auto s0 = frame_starts_[static_cast<size_t>(frame & kFrameMapMask)];
600 const auto s1 = frame_starts_[static_cast<size_t>((frame + 1) & kFrameMapMask)];
601 const auto offset = final_input_count_ - frame * hop;
602 const long double mapped = static_cast<long double>(s0)
603 + static_cast<long double>(s1 - s0)
604 * static_cast<long double>(offset)
605 / static_cast<long double>(hop);
606 final_output_limit_ = static_cast<std::int64_t>(std::llround(mapped));
607 assert(final_output_limit_ >= stretch_written_);
608 }
609
610 // Fractional read of the stretched ring at read_pos_ — Catmull-Rom
611 // cubic by default, or a Kaiser-windowed sinc kernel when enabled (lower
612 // aliasing on the pitch-shift resample step).
613 void read_fractional(SampleType* const* out, int i) {
614 if (stretch_written_ < 4) {
615 for (int ch = 0; ch < config_.channels; ++ch) out[ch][i] = SampleType{0};
616 return;
617 }
618 const auto i1 = static_cast<std::int64_t>(read_pos_);
619 const SampleType t = static_cast<SampleType>(read_pos_ - static_cast<double>(i1));
620 const std::int64_t last = stretch_written_ - 1;
621 const auto clamp_idx = [&](std::int64_t p) {
622 return static_cast<size_t>(std::clamp<std::int64_t>(p, 0, last) & ring_mask_);
623 };
624 if (config_.sinc_resampling) {
625 const int taps = resampler_.taps();
626 const int half = resampler_.half_width();
627 for (int ch = 0; ch < config_.channels; ++ch) {
628 const SampleType* ring = stretch_ring_.data()
629 + static_cast<size_t>(ch) * ring_size_;
630 // Gather the kernel neighbourhood (i1-half+1 .. i1+half) from
631 // the ring with edge clamping, then apply the sinc kernel.
632 for (int k = 0; k < taps; ++k)
633 tap_scratch_[static_cast<size_t>(k)] =
634 ring[clamp_idx(i1 + k - half + 1)];
635 out[ch][i] = resampler_.apply(tap_scratch_.data(), t);
636 }
637 return;
638 }
639 for (int ch = 0; ch < config_.channels; ++ch) {
640 const SampleType* ring = stretch_ring_.data()
641 + static_cast<size_t>(ch) * ring_size_;
642 const SampleType p0 = ring[clamp_idx(i1 - 1)];
643 const SampleType p1 = ring[clamp_idx(i1)];
644 const SampleType p2 = ring[clamp_idx(i1 + 1)];
645 const SampleType p3 = ring[clamp_idx(i1 + 2)];
646 const SampleType a = static_cast<SampleType>(0.5)
647 * (-p0 + static_cast<SampleType>(3) * p1
648 - static_cast<SampleType>(3) * p2 + p3);
649 const SampleType b = p0 - static_cast<SampleType>(2.5) * p1
650 + static_cast<SampleType>(2) * p2
651 - static_cast<SampleType>(0.5) * p3;
652 const SampleType c = static_cast<SampleType>(0.5) * (p2 - p0);
653 out[ch][i] = ((a * t + b) * t + c) * t + p1;
654 }
655 }
656
657 const SampleType* const* advance_ptrs(const SampleType* const* in, int offset) {
658 for (int ch = 0; ch < config_.channels; ++ch)
659 in_ptrs_scratch_[ch] = in[ch] + offset;
660 return in_ptrs_scratch_;
661 }
662
663 static constexpr int kReadGuard = 8; // frame-map + cubic-interp headroom
664 static constexpr int kFrameMapMask = 63;
665
666 RealtimePitchTimeConfig config_;
667 double sample_rate_ = 48000.0;
668 int fft_size_ = 4096;
669 int analysis_hop_ = 512;
670 SampleType time_ratio_ = SampleType{1};
671
672 SpectralFrameEngineT<SampleType> engine_;
673 MultichannelPhaseCoordinatorT<SampleType> coordinator_;
674 SpectralEnvelopeShifterT<SampleType> envelope_;
675 TransientPhasePolicyT<SampleType> transient_;
676 FreezeHoldT<SampleType> freeze_;
677 LatencyAwareControlSmoother pitch_smoother_;
678 LatencyAwareControlSmoother formant_smoother_;
679
680 // Noise-morphing front end (allocated only when config_.noise_morphing).
681 StnDecomposerT<SampleType> stn_;
682 std::vector<NoiseMorpherT<SampleType>> noise_morphers_;
683 std::vector<SampleType> mag_scratch_;
684 std::vector<SampleType> noise_env_; // channels * spectral_bins
685 std::vector<std::complex<SampleType>> noise_spec_;
686
687 // Sinc resampler (allocated only when config_.sinc_resampling).
688 SincResamplerT<SampleType> resampler_;
689 std::vector<SampleType> tap_scratch_; // resampler_.taps() gather buffer
690
691 int ring_size_ = 0;
692 int ring_mask_ = 0;
693 std::vector<SampleType> stretch_ring_;
694 std::vector<SampleType> drain_buf_;
695 std::vector<SampleType*> drain_ptrs_;
696 std::vector<SampleType> finalize_zero_buf_;
697 std::vector<const SampleType*> finalize_zero_ptrs_;
698 const SampleType* in_ptrs_scratch_[kRealtimePitchTimeMaximumChannels] = {};
699
700 detail::FractionalSynthesisHopAccumulator synth_hop_accumulator_;
701 std::int64_t synth_accum_int_ = 0;
702 std::int64_t frames_done_ = 0;
703 std::int64_t stretch_written_ = 0;
704 std::int64_t stretch_read_ = 0;
705 double read_pos_ = 0.0;
706 std::int64_t frame_starts_[kFrameMapMask + 1] = {};
707 std::int64_t out_count_ = 0;
708 std::int64_t input_count_ = 0;
709 int offset_in_block_ = 0;
710 int max_synthesis_hop_ = 0;
711 int maximum_stream_output_lag_samples_ = 0;
712 bool input_closed_ = false;
713 std::int64_t final_input_count_ = 0;
714 std::int64_t final_output_limit_ = -1;
715 int finalize_padding_remaining_ = 0;
716};
717
720
721} // namespace pulp::signal
@ semitone
Definition latency_aware_control_smoother.hpp:33
Definition realtime_pitch_time_processor.hpp:59
double achieved_time_ratio() const
Hop-quantized stretch actually applied so far (time_stretch mode).
Definition realtime_pitch_time_processor.hpp:213
int read_stretched(SampleType *const *out, int num_samples)
time_stretch mode: pop stretched samples (caller respects available_stretched(); excess is zero-fille...
Definition realtime_pitch_time_processor.hpp:384
void set_pitch_semitones(float semitones)
Definition realtime_pitch_time_processor.hpp:188
PitchTimePrepareStatus prepare(double sample_rate, const RealtimePitchTimeConfig &config, std::uint64_t target_max_bytes=kTargetAddressMaximumBytes)
RT contract: prepare() allocates and sizes all spectral, smoothing, ring, drain, noise-morphing,...
Definition realtime_pitch_time_processor.hpp:70
int samples_until_next_analysis_frame() const noexcept
Samples remaining before the next analysis frame completes.
Definition realtime_pitch_time_processor.hpp:311
void set_time_ratio(float ratio)
time_stretch mode only; > 1 lengthens. Takes effect at the next frame.
Definition realtime_pitch_time_processor.hpp:207
int input_priming_samples() const
Input required before the first final stretched sample can become readable.
Definition realtime_pitch_time_processor.hpp:298
PitchTimeStreamFinalizeStatus finalize(int max_samples)
Bounded finalization variant.
Definition realtime_pitch_time_processor.hpp:352
int latency_samples() const
Fixed pipeline delay of process() in realtime_pitch mode.
Definition realtime_pitch_time_processor.hpp:185
int maximum_stream_output_lag_samples() const noexcept
Admission-time causal stream delay bound in output samples for every ratio accepted by set_time_ratio...
Definition realtime_pitch_time_processor.hpp:303
void reset()
Definition realtime_pitch_time_processor.hpp:402
int available_stretched() const
time_stretch mode: stretched samples ready to read.
Definition realtime_pitch_time_processor.hpp:283
void set_formant_semitones(float semitones)
Definition realtime_pitch_time_processor.hpp:193
PitchTimeStreamFinalizeStatus finalize()
Seal the finite input stream and advance at most one prepared input block of zero padding.
Definition realtime_pitch_time_processor.hpp:322
int output_free_space() const
Remaining prepared output-ring capacity.
Definition realtime_pitch_time_processor.hpp:289
PitchTimeStreamFeedStatus feed(const SampleType *const *in, int num_samples)
time_stretch mode: atomically push one bounded input block.
Definition realtime_pitch_time_processor.hpp:269
int output_alignment_samples() const
Definition realtime_pitch_time_processor.hpp:299
void set_formant_mode(FormantMode mode)
Definition realtime_pitch_time_processor.hpp:198
PitchTimeStreamFinalizePlan plan_finalize(int max_samples) const noexcept
Allocation-free, non-mutating preflight for bounded finalization.
Definition realtime_pitch_time_processor.hpp:330
int fft_size() const
Definition realtime_pitch_time_processor.hpp:186
void set_frozen(bool frozen)
Freeze (infinite hold) of the current input moment.
Definition realtime_pitch_time_processor.hpp:203
bool is_frozen() const
Definition realtime_pitch_time_processor.hpp:204
void process(const SampleType *const *in, SampleType *const *out, int num_samples)
realtime_pitch mode: consume and produce exactly num_samples.
Definition realtime_pitch_time_processor.hpp:221
Definition spectral_envelope_shifter.hpp:51
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
void analyze(const SampleType *const *in, int num_samples, Fn &&on_frames)
Split API — analysis only.
Definition spectral_frame_engine.hpp:524
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
Spectral freeze / infinite hold for phase-vocoder frame groups.
Sample-accurate, block-size-independent control smoothing with independent attack/release time consta...
Phase-vocoder phase propagation for coherent channel groups.
@ publish
Definition component.hpp:89
@ f
Definition pitch.hpp:25
@ b
Definition pitch.hpp:31
@ ring
Definition string.hpp:18
@ half
Definition units.hpp:290
Definition additive_bank.hpp:218
@ Ok
Definition source_filter_analysis.hpp:29
@ realtime_pitch
Definition realtime_pitch_time_geometry.hpp:26
@ time_stretch
Definition realtime_pitch_time_geometry.hpp:26
RealtimePitchTimeProcessorT< float > RealtimePitchTimeProcessor
Definition realtime_pitch_time_processor.hpp:718
@ c
Definition spectrum_trace.hpp:35
RealtimePitchTimeProcessorT< double > RealtimePitchTimeProcessor64
Definition realtime_pitch_time_processor.hpp:719
@ needs_drain
Definition realtime_pitch_time_geometry.hpp:39
@ invalid_mode
Definition realtime_pitch_time_geometry.hpp:41
@ invalid_request
Definition realtime_pitch_time_geometry.hpp:42
@ ready
Definition realtime_pitch_time_geometry.hpp:38
@ complete
Definition realtime_pitch_time_geometry.hpp:40
PitchTimeStreamFinalizeStatus
Definition realtime_pitch_time_geometry.hpp:30
@ invalid_mode
Definition realtime_pitch_time_geometry.hpp:34
@ draining
Definition realtime_pitch_time_geometry.hpp:31
@ invalid_request
Definition realtime_pitch_time_geometry.hpp:35
@ backpressure
Definition realtime_pitch_time_geometry.hpp:32
@ complete
Definition realtime_pitch_time_geometry.hpp:33
@ quality
Definition realtime_pitch_time_geometry.hpp:25
@ ratio
base_frequency_hz * ratio.
Definition fm_operator_engine.hpp:20
constexpr int kRealtimePitchTimeMaximumChannels
Definition realtime_pitch_time_geometry.hpp:79
constexpr std::uint64_t kTargetAddressMaximumBytes
Definition checked_allocation.hpp:9
@ a
Definition formant_filter_bank.hpp:20
@ i
Definition formant_filter_bank.hpp:20
@ e
Definition formant_filter_bank.hpp:20
PitchTimePrepareStatus
Definition realtime_pitch_time_geometry.hpp:50
@ unrepresentable_capacity
Definition realtime_pitch_time_geometry.hpp:58
@ invalid_channel_count
Definition realtime_pitch_time_geometry.hpp:53
@ invalid_sample_rate
Definition realtime_pitch_time_geometry.hpp:52
@ invalid_max_time_ratio
Definition realtime_pitch_time_geometry.hpp:55
@ prepared
Definition realtime_pitch_time_geometry.hpp:51
@ invalid_max_pitch_semitones
Definition realtime_pitch_time_geometry.hpp:56
@ invalid_max_block
Definition realtime_pitch_time_geometry.hpp:54
PitchTimeStreamFeedStatus
Definition realtime_pitch_time_geometry.hpp:29
@ accepted
Definition realtime_pitch_time_geometry.hpp:29
@ invalid_request
Definition realtime_pitch_time_geometry.hpp:29
@ input_closed
Definition realtime_pitch_time_geometry.hpp:29
@ backpressure
Definition realtime_pitch_time_geometry.hpp:29
FormantMode
Definition realtime_pitch_time_geometry.hpp:27
@ preserve
Definition realtime_pitch_time_geometry.hpp:27
PitchTimePrepareStatus checked_realtime_pitch_time_prepared_geometry(const RealtimePitchTimeConfig &config, double max_pitch_ratio, std::uint64_t requested_max_bytes, RealtimePitchTimePreparedGeometry< SampleType > &prepared) noexcept
Derive all processor geometry and compose the retained-storage contracts of its owned DSP primitives ...
Definition realtime_pitch_time_geometry.hpp:101
@ m
Definition input_events.hpp:40
@ p
Definition input_events.hpp:40
@ k
Definition input_events.hpp:40
@ t
Definition input_events.hpp:41
@ stretch
Definition design_ir.hpp:57
Time-stretch the noise component of a sound by morphing, not by phase-vocoder propagation.
Pure configuration and retained-storage admission for the realtime pitch/time processor.
Fractional-delay resampling by a Kaiser-windowed sinc kernel.
Spectral-envelope estimation and formant warping for phase-vocoder frame groups.
Streaming STFT analysis + weighted overlap-add (WOLA) synthesis for DSP-grade spectral processing — d...
Sines / Transients / Noise (STN) decomposition of a spectral frame stream by median-filter masking.
Two sizing modes.
Definition freeze_hold.hpp:410
int fft_size
Definition freeze_hold.hpp:411
int analysis_hop
Definition freeze_hold.hpp:413
int channels
Definition freeze_hold.hpp:412
Definition latency_aware_control_smoother.hpp:35
float attack_seconds
Definition latency_aware_control_smoother.hpp:37
float release_seconds
Definition latency_aware_control_smoother.hpp:38
Domain domain
Definition latency_aware_control_smoother.hpp:36
Definition realtime_pitch_time_geometry.hpp:45
Definition realtime_pitch_time_geometry.hpp:61
Definition realtime_pitch_time_geometry.hpp:82
std::uint64_t finalize_zero_elements
Definition realtime_pitch_time_geometry.hpp:94
std::uint64_t drain_elements
Definition realtime_pitch_time_geometry.hpp:93
int maximum_stream_output_lag_samples
Conservative output-domain delay required by a causal time-stretch stream over the complete admitted ...
Definition realtime_pitch_time_geometry.hpp:91
int ring_size
Definition realtime_pitch_time_geometry.hpp:86
int fft_size
Definition realtime_pitch_time_geometry.hpp:84
SpectralFrameEngineConfig engine_config
Definition realtime_pitch_time_geometry.hpp:83
int analysis_hop
Definition realtime_pitch_time_geometry.hpp:85
std::uint64_t stretch_ring_elements
Definition realtime_pitch_time_geometry.hpp:92
Definition spectral_envelope_shifter.hpp:38
int fft_size
Definition spectral_envelope_shifter.hpp:39
int true_envelope_iterations
True-envelope refinement passes (0 = plain liftering).
Definition spectral_envelope_shifter.hpp:45
int max_block
Definition spectral_frame_engine.hpp:60
int max_synthesis_hop
Definition spectral_frame_engine.hpp:61
Definition stn_decomposer.hpp:39
int freq_median
odd; bins for the vertical (transient) median
Definition stn_decomposer.hpp:42
bool causal
Align the masks to the NEWEST pushed frame instead of the ring's center frame.
Definition stn_decomposer.hpp:59
int num_bins
fft_size/2 + 1
Definition stn_decomposer.hpp:40
int time_median
odd; frames for the horizontal (tonal) median
Definition stn_decomposer.hpp:41
Definition transient_phase_policy.hpp:38
int fft_size
Definition transient_phase_policy.hpp:39
float sensitivity
Definition transient_phase_policy.hpp:41
Spectral-flux transient detection for phase-vocoder streams.