Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
cyclic_stretch.hpp
Go to the documentation of this file.
1#pragma once
2
206
209#include <pulp/signal/units.hpp>
210
211#include <algorithm>
212#include <cmath>
213#include <cstddef>
214#include <cstdint>
215#include <vector>
216
217namespace pulp::signal {
218
227inline long long round_half_even(double x) noexcept {
228 const double floored = std::floor(x);
229 const double fraction = x - floored;
230 const auto below = static_cast<long long>(floored);
231 if (fraction > 0.5) return below + 1;
232 if (fraction < 0.5) return below;
233 return (below & 1LL) == 0 ? below : below + 1; // tie → the even neighbour
234}
235
241 double cycle_hz = 200.0;
243 double crossfade_pct = 25.0;
244 double crossfade_shape = 1.0;
245};
246
253inline constexpr CyclicStretchRegime kCyclicStretchShortFrame{200.0, 1, 45.0, 1.0};
254
261inline constexpr CyclicStretchRegime kCyclicStretchLongFrame{200.0, 8, 8.0, 0.0};
262
264template <typename SampleType = float>
266public:
267 // ── Design parameters (the complete roster) ───────────────────────────
268
273 static constexpr double kCycleHzMin = 20.0;
274 static constexpr double kCycleHzMax = 2000.0;
275 static constexpr double kCycleHzDefault = 200.0;
276
278 static constexpr int kGrainPeriodsMin = 1;
279 static constexpr int kGrainPeriodsMax = 16;
280 static constexpr int kGrainPeriodsDefault = 1;
281
286 static constexpr double kCrossfadePctMin = 1.0;
287 static constexpr double kCrossfadePctMax = 50.0;
288 static constexpr double kCrossfadePctDefault = 25.0;
289
292 static constexpr double kCrossfadeShapeDefault = 1.0;
293
299 static constexpr double kStretchRatioMin = 0.25;
300 static constexpr double kStretchRatioMax = 4.0;
301 static constexpr double kStretchRatioDefault = 1.0;
302
306 static constexpr double kCaptureMsMin = 250.0;
307 static constexpr double kCaptureMsMax = 8000.0;
308 static constexpr double kCaptureMsDefault = 2000.0;
309
312 static constexpr double kMixDefault = 100.0;
313
317 static constexpr double kOutputDbMin = -24.0;
318 static constexpr double kOutputDbMax = 12.0;
319 static constexpr double kOutputDbDefault = 0.0;
320
325 static constexpr int kMinCycleSamples = 4;
326
330 static constexpr int kMinHopSamples = 2;
331
334 static constexpr double kWorstCaseGain = 1.41421356237309504880;
335
336 // ── Lifecycle ─────────────────────────────────────────────────────────
337
341 void prepare(double sample_rate) {
342 if (std::isfinite(sample_rate) && sample_rate > 0.0) sample_rate_ = sample_rate;
343
344 // Sized for the extremes of the declared ranges, not for the current
345 // settings: `set_capture_ms` and `set_cycle_hz` must never reallocate,
346 // and the only way to promise that is to buy the worst case once.
347 const auto ring_needed = static_cast<std::size_t>(
348 std::ceil(units::ms_to_samples(kCaptureMsMax, sample_rate_))) + 2u;
349 ring_mask_ = power_of_two_mask(ring_needed);
350 ring_.assign(ring_mask_ + 1u, SampleType{0});
351
352 const auto longest_cycle = static_cast<long long>(
353 std::llround(sample_rate_ / kCycleHzMin));
354 max_grain_ = std::max<long long>(
355 1, longest_cycle * kGrainPeriodsMax);
356 // A grain writes `N` samples starting at the current write cursor, and
357 // that cursor never runs more than `N` ahead of the read cursor (grains
358 // are produced only as far as the current output sample needs). So the
359 // live span is `N`, and twice that leaves every slot a full lap between
360 // being zeroed and being written again.
361 accum_mask_ = power_of_two_mask(static_cast<std::size_t>(2 * max_grain_) + 2u);
362 accum_.assign(accum_mask_ + 1u, 0.0);
363 accum_epoch_.assign(accum_mask_ + 1u, 0u);
364
365 update_lengths();
366 reset();
367 }
368
370 void reset() {
371 std::fill(ring_.begin(), ring_.end(), SampleType{0});
372 std::fill(accum_.begin(), accum_.end(), 0.0);
373 std::fill(accum_epoch_.begin(), accum_epoch_.end(), 0u);
374 accum_generation_ = 1u;
375 reset_runtime_state();
376 }
377
378private:
379 void audio_fault_reset() noexcept {
380 ++accum_generation_;
381 if (accum_generation_ == 0u) accum_generation_ = 1u;
382 reset_runtime_state();
383 }
384
385 void reset_runtime_state() noexcept {
386 write_index_ = 0;
387 total_written_ = 0;
388 out_read_ = 0;
389 grain_ = 0;
390 grain_out_pos_ = 0;
391 read_pos_ = 0;
392 prev_out_pos_ = 0;
393 previous_fade_out_ = crossfade_length_;
394 end_previous_grain_ = 0;
395 end_two_grains_ago_ = 0;
396 }
397
398public:
399
400 // ── Parameters ────────────────────────────────────────────────────────
401 //
402 // None of these resets the schedule or the ring. That is deliberate: a live
403 // automation move on `cycle_hz` should slide the buzz, not restart the
404 // machine with a click. `reset()` is the only full clear.
405
406 void set_cycle_hz(double hz) {
407 if (!std::isfinite(hz)) return;
408 cycle_hz_ = std::clamp(hz, kCycleHzMin, kCycleHzMax);
409 update_lengths();
410 }
411
412 void set_grain_periods(int periods) {
413 grain_periods_ = std::clamp(periods, kGrainPeriodsMin, kGrainPeriodsMax);
414 update_lengths();
415 }
416
417 void set_crossfade_pct(double pct) {
418 if (!std::isfinite(pct)) return;
419 crossfade_pct_ = std::clamp(pct, kCrossfadePctMin, kCrossfadePctMax);
420 update_lengths();
421 }
422
423 void set_crossfade_shape(double shape) noexcept {
424 if (!std::isfinite(shape)) return;
425 crossfade_shape_ = std::clamp(shape, 0.0, 1.0);
426 }
427
428 void set_stretch_ratio(double ratio) noexcept {
429 if (!std::isfinite(ratio)) return;
430 stretch_ratio_ = std::clamp(ratio, kStretchRatioMin, kStretchRatioMax);
431 }
432
433 void set_capture_ms(double ms) {
434 if (!std::isfinite(ms)) return;
435 capture_ms_ = std::clamp(ms, kCaptureMsMin, kCaptureMsMax);
436 update_lengths();
437 }
438
439 void set_mix(double percent) noexcept {
440 if (!std::isfinite(percent)) return;
441 mix_ = std::clamp(percent, 0.0, 100.0) * 0.01;
442 }
443
444 void set_output_db(double db) noexcept {
445 if (!std::isfinite(db)) return;
446 output_gain_ = units::db_to_linear(std::clamp(db, kOutputDbMin, kOutputDbMax));
447 }
448
456
457 // ── Resolved lengths ──────────────────────────────────────────────────
458
460 int cycle_samples() const noexcept { return static_cast<int>(cycle_length_); }
462 int grain_samples() const noexcept { return static_cast<int>(grain_length_); }
464 int crossfade_samples() const noexcept { return static_cast<int>(crossfade_length_); }
466 int hop_samples() const noexcept { return static_cast<int>(hop_length_); }
468 double flutter_hz() const noexcept {
469 return hop_length_ > 0 ? sample_rate_ / static_cast<double>(hop_length_) : 0.0;
470 }
471
473 long long capture_window_samples() const noexcept { return capture_window_; }
474
475 double sample_rate() const noexcept { return sample_rate_; }
476 double stretch_ratio() const noexcept { return stretch_ratio_; }
477 double crossfade_shape() const noexcept { return crossfade_shape_; }
478
483 int latency_samples() const noexcept { return static_cast<int>(grain_length_); }
484
488 double worst_case_gain() const noexcept {
489 return (1.0 - crossfade_shape_) + crossfade_shape_ * kWorstCaseGain;
490 }
491
497 long long schedule_input_position(long long out_pos) const noexcept {
498 const double ideal = static_cast<double>(out_pos) / stretch_ratio_;
499 return cycle_length_ * round_half_even(ideal / static_cast<double>(cycle_length_));
500 }
501
506 long long grain_input_position(long long g) const noexcept {
507 return schedule_input_position(g * hop_length_);
508 }
509
511 long long grain_count() const noexcept { return grain_; }
512
514 long long next_grain_out_pos() const noexcept { return grain_out_pos_; }
515
522 long long read_position() const noexcept { return read_pos_; }
523
525 long long total_captured() const noexcept { return total_written_; }
526
527 // ── Audio ─────────────────────────────────────────────────────────────
528
532 void process(const SampleType* in, SampleType* out, int n) {
533 if (n <= 0) return;
534 if (ring_.empty() || accum_.empty() || in == nullptr) {
535 std::fill_n(out, n, SampleType{0});
536 return;
537 }
538 for (int i = 0; i < n; ++i) {
539 if (!std::isfinite(static_cast<double>(in[i]))) {
540 audio_fault_reset();
541 out[i] = SampleType{0};
542 continue;
543 }
544 ring_[write_index_] = in[i];
545 write_index_ = (write_index_ + 1u) & ring_mask_;
546 ++total_written_;
547
548 // The initial fill. Grain 0 cannot exist until its N source samples
549 // do, and that wait IS the reported latency.
550 if (total_written_ <= grain_length_) {
551 out[i] = SampleType{0};
552 continue;
553 }
554
555 // Produce exactly the grains the current output sample depends on.
556 // Output sample p is final once every grain starting at or before p
557 // has been written, because a later grain starts later than p by
558 // construction.
559 while (grain_out_pos_ <= out_read_) emit_grain();
560
561 const auto slot = static_cast<std::size_t>(out_read_) & accum_mask_;
562 const double wet = accum_epoch_[slot] == accum_generation_
563 ? snap_to_zero(accum_[slot])
564 : 0.0;
565 accum_[slot] = 0.0;
566 accum_epoch_[slot] = accum_generation_;
567 ++out_read_;
568
569 // The dry path is delayed by exactly N, so `mix` blends two things
570 // that are talking about the same moment. The `+ 1` is load-bearing
571 // and was measured, not reasoned: `write_index_` has ALREADY been
572 // advanced past the sample just captured, so the newest sample sits
573 // at `write_index_ - 1`, and reading `write_index_ - N` delays by
574 // N−1. That off-by-one is invisible on a tone and shows up as a
575 // one-sample pre-echo on an impulse at `mix` between 0 and 1.
576 const auto dry_index = static_cast<std::size_t>(
577 (write_index_ + ring_mask_ + 1u -
578 static_cast<std::size_t>(grain_length_ + 1)) & ring_mask_);
579 const double dry = static_cast<double>(ring_[dry_index]);
580
581 const double result = output_gain_ * (mix_ * wet + (1.0 - mix_) * dry);
582 if (!std::isfinite(result)) {
583 audio_fault_reset();
584 out[i] = SampleType{0};
585 } else {
586 out[i] = static_cast<SampleType>(result);
587 }
588 }
589 }
590
591private:
592 static std::size_t power_of_two_mask(std::size_t at_least) {
593 std::size_t size = 2;
594 while (size < at_least) size <<= 1;
595 return size - 1u;
596 }
597
634 void advance_read_cursor(long long out_pos) noexcept {
635 read_pos_ += schedule_input_position(out_pos) -
636 schedule_input_position(prev_out_pos_);
637 prev_out_pos_ = out_pos;
638
639 const long long newest = total_written_ - grain_length_;
640 const long long oldest = std::max<long long>(0, total_written_ - capture_window_);
641 const long long span = std::max<long long>(cycle_length_, newest - oldest);
642
643 if (read_pos_ > newest) {
644 const long long laps = (read_pos_ - newest + span - 1) / span;
645 read_pos_ -= laps * span;
646 } else if (read_pos_ < oldest) {
647 const long long laps = (oldest - read_pos_ + span - 1) / span;
648 read_pos_ += laps * span;
649 }
650
651 // Grid-snap, then a final hard clamp: the snap can push a position that
652 // was exactly on an edge one cycle past it.
653 read_pos_ = (read_pos_ / cycle_length_) * cycle_length_;
654 read_pos_ = std::clamp(read_pos_, std::max<long long>(0, oldest),
655 std::max<long long>(0, newest));
656 }
657
662 void splice_weights(double theta, double& fade_in, double& fade_out) const noexcept {
663 double linear_out = 0.0, linear_in = 0.0;
664 double power_out = 0.0, power_in = 0.0;
665 crossfade_gains(theta, CrossfadeGainLaw::EqualGain, linear_out, linear_in);
666 crossfade_gains(theta, CrossfadeGainLaw::EqualPower, power_out, power_in);
667 fade_in = linear_in + crossfade_shape_ * (power_in - linear_in);
668 fade_out = linear_out + crossfade_shape_ * (power_out - linear_out);
669 }
670
672 void emit_grain() noexcept {
673 // A grain may not start before the one BEFORE its predecessor has
674 // finished. That is the two-grain overlap invariant stated directly,
675 // and it is what every gain bound in this file rests on.
676 //
677 // At settled lengths it provably never binds: the grain two back ended
678 // at `out_pos − 2S + N = out_pos − N + 2X`, and `2X ≤ N` is clamped, so
679 // it ended at or before `out_pos` already. It binds only while the
680 // length controls are being swept, which is exactly when a long grain
681 // can still be sounding while short grains march past it at a hop that
682 // has nothing to do with the length of the one in flight. Measured
683 // without this guard, a full-range per-block sweep of every length
684 // control reached 2.35× — well past the √2 the design proves — because
685 // four and five grains were summing at once.
686 const long long out_pos = std::max(grain_out_pos_, end_two_grains_ago_);
687 grain_out_pos_ = out_pos;
688 advance_read_cursor(out_pos);
689 const long long in_pos = read_pos_;
690
691 // This grain fades IN over exactly as many samples as its predecessor
692 // faded OUT, not over its own `X`.
693 //
694 // At settled parameters those are the same number and this is a no-op.
695 // They differ only while `crossfade_pct`, `cycle_hz` or `grain_periods`
696 // are being automated, and that is precisely when it matters: the two
697 // halves of a splice have to be COMPLEMENTARY for their weights to sum
698 // to the bound the design proves. Give a splice a short fade-out and a
699 // long fade-in and the outgoing grain is still at unity while the
700 // incoming one is already most of the way up — the pair sums toward 2,
701 // not √2, and the level jumps audibly on an automation move. Matching
702 // the pair to the outgoing side keeps every splice complementary no
703 // matter how violently the lengths are swept.
704 // The first grain has no predecessor to match, so it uses the current
705 // overlap. Deriving that from `grain_` rather than from a value seeded
706 // at `reset()` is deliberate: a setter called AFTER `prepare()` — which
707 // is the normal way to configure this thing — would otherwise leave the
708 // seeded value describing the lengths that were in force during
709 // `prepare`, and a later `reset()` would re-seed it from the CURRENT
710 // lengths instead. Two renders that should be identical then differ in
711 // their very first grain.
712 const long long previous_fade =
713 grain_ == 0 ? crossfade_length_ : previous_fade_out_;
714 const long long fade_in = std::clamp<long long>(
715 previous_fade, 1, std::max<long long>(1, grain_length_ - crossfade_length_));
716 const long long fade_out = crossfade_length_;
717 const long long interior_start = grain_length_ - fade_out;
718
719 for (long long k = 0; k < grain_length_; ++k) {
720 const auto source = static_cast<std::size_t>(in_pos + k) & ring_mask_;
721 double weight = 1.0;
722 if (k < fade_in) {
723 double rising = 0.0, falling = 0.0;
724 splice_weights(static_cast<double>(k) / static_cast<double>(fade_in),
725 rising, falling);
726 weight = rising;
727 } else if (k >= interior_start) {
728 double rising = 0.0, falling = 0.0;
729 splice_weights(static_cast<double>(k - interior_start) /
730 static_cast<double>(fade_out),
731 rising, falling);
732 weight = falling;
733 }
734 const auto slot = static_cast<std::size_t>(out_pos + k) & accum_mask_;
735 if (accum_epoch_[slot] != accum_generation_) {
736 accum_[slot] = 0.0;
737 accum_epoch_[slot] = accum_generation_;
738 }
739 accum_[slot] += static_cast<double>(ring_[source]) * weight;
740 }
741
742 // Advance so the NEXT grain's fade-in lands exactly on this grain's
743 // fade-out. The `fade_in` floor is the anti-pile-up guard: without it a
744 // hop that has just been automated far below the previous grain's fade
745 // length would let a third grain start before the first had finished,
746 // and three overlapping grains break the two-term sum the bound rests
747 // on. At settled parameters `fade_in ≤ X ≤ N/2 ≤ S`, so the floor never
748 // binds and the hop is exactly `S`.
749 grain_out_pos_ += std::max(hop_length_, fade_in);
750 previous_fade_out_ = fade_out;
751 end_two_grains_ago_ = end_previous_grain_;
752 end_previous_grain_ = out_pos + grain_length_;
753 ++grain_;
754 }
755
759 void update_lengths() noexcept {
760 const double max_cycle_hz = sample_rate_ / static_cast<double>(kMinCycleSamples);
761 const double hz = std::min(cycle_hz_, max_cycle_hz);
762 cycle_length_ = std::max<long long>(kMinCycleSamples,
763 std::llround(sample_rate_ / hz));
764
765 grain_length_ = cycle_length_ * grain_periods_;
766 if (max_grain_ > 0) grain_length_ = std::min(grain_length_, max_grain_);
767
768 crossfade_length_ = std::llround(crossfade_pct_ * 0.01 *
769 static_cast<double>(grain_length_));
770 // The 50 % ceiling keeps the overlap-add to two grains, which is what
771 // both the interior-unity assumption and the √2 bound rest on.
772 crossfade_length_ = std::clamp<long long>(crossfade_length_, 1, grain_length_ / 2);
773
774 hop_length_ = std::max<long long>(kMinHopSamples,
775 grain_length_ - crossfade_length_);
776
777 capture_window_ = std::max<long long>(
778 grain_length_ + 1,
779 static_cast<long long>(std::llround(
780 units::ms_to_samples(capture_ms_, sample_rate_))));
781 capture_window_ = std::min<long long>(
782 capture_window_, static_cast<long long>(ring_mask_));
783 }
784
785 // Controls.
786 double sample_rate_ = 48000.0;
787 double cycle_hz_ = kCycleHzDefault;
788 int grain_periods_ = kGrainPeriodsDefault;
789 double crossfade_pct_ = kCrossfadePctDefault;
790 double crossfade_shape_ = kCrossfadeShapeDefault;
791 double stretch_ratio_ = kStretchRatioDefault;
792 double capture_ms_ = kCaptureMsDefault;
793 double mix_ = kMixDefault * 0.01;
794 double output_gain_ = 1.0;
795
796 // Resolved lengths.
797 long long cycle_length_ = 240;
798 long long grain_length_ = 240;
799 long long crossfade_length_ = 60;
800 long long hop_length_ = 180;
801 long long capture_window_ = 96000;
802 long long max_grain_ = 0;
803
804 // Buffers and cursors. `total_written_` and `out_read_` are ABSOLUTE, which
805 // is what makes the render independent of block size.
811 std::vector<SampleType> ring_{};
812 std::vector<double> accum_{};
813 std::vector<std::uint64_t> accum_epoch_{};
814 std::uint64_t accum_generation_ = 1u;
815 std::size_t ring_mask_ = 0;
816 std::size_t accum_mask_ = 0;
817 std::size_t write_index_ = 0;
818 long long total_written_ = 0;
819 long long out_read_ = 0;
820 long long grain_ = 0;
821
833 long long grain_out_pos_ = 0;
834
838 long long read_pos_ = 0;
839 long long prev_out_pos_ = 0;
840
844 long long previous_fade_out_ = 60;
845
849 long long end_previous_grain_ = 0;
850 long long end_two_grains_ago_ = 0;
851};
852
855
856} // namespace pulp::signal
The fixed-cycle splice stretcher.
Definition cyclic_stretch.hpp:265
double stretch_ratio() const noexcept
Definition cyclic_stretch.hpp:476
double flutter_hz() const noexcept
The metallic flutter fundamental, fs / S, in Hz.
Definition cyclic_stretch.hpp:468
static constexpr double kStretchRatioMax
Definition cyclic_stretch.hpp:300
static constexpr int kGrainPeriodsDefault
Definition cyclic_stretch.hpp:280
static constexpr double kStretchRatioDefault
Definition cyclic_stretch.hpp:301
void set_regime(const CyclicStretchRegime &regime)
Apply one of the two documented regimes in a single call.
Definition cyclic_stretch.hpp:450
static constexpr double kCaptureMsMax
Definition cyclic_stretch.hpp:307
static constexpr double kOutputDbMin
Definition cyclic_stretch.hpp:317
static constexpr double kCycleHzDefault
Definition cyclic_stretch.hpp:275
static constexpr double kMixDefault
Definition cyclic_stretch.hpp:312
void reset()
Never allocates. A zero-initialised instance is already in this state.
Definition cyclic_stretch.hpp:370
void prepare(double sample_rate)
Sizes the capture ring for kCaptureMsMax and the accumulator for the largest grain the parameter rang...
Definition cyclic_stretch.hpp:341
void set_grain_periods(int periods)
Definition cyclic_stretch.hpp:412
static constexpr int kGrainPeriodsMax
Definition cyclic_stretch.hpp:279
void set_stretch_ratio(double ratio) noexcept
Definition cyclic_stretch.hpp:428
void set_capture_ms(double ms)
Definition cyclic_stretch.hpp:433
static constexpr double kCaptureMsDefault
Definition cyclic_stretch.hpp:308
void set_output_db(double db) noexcept
Definition cyclic_stretch.hpp:444
double worst_case_gain() const noexcept
The proven bound on |out| / max|in| — see the file doc block.
Definition cyclic_stretch.hpp:488
static constexpr int kMinCycleSamples
Definition cyclic_stretch.hpp:325
int crossfade_samples() const noexcept
X — splice overlap in samples.
Definition cyclic_stretch.hpp:464
long long grain_input_position(long long g) const noexcept
The schedule at grain index g, valid while the resolved lengths hold still — out_pos(g) = g·S only as...
Definition cyclic_stretch.hpp:506
void set_crossfade_pct(double pct)
Definition cyclic_stretch.hpp:417
int cycle_samples() const noexcept
L — cycle length in samples.
Definition cyclic_stretch.hpp:460
int hop_samples() const noexcept
S — synthesis hop in samples.
Definition cyclic_stretch.hpp:466
static constexpr double kCrossfadePctMin
Definition cyclic_stretch.hpp:286
static constexpr double kCycleHzMin
Definition cyclic_stretch.hpp:273
static constexpr double kWorstCaseGain
Definition cyclic_stretch.hpp:334
void set_crossfade_shape(double shape) noexcept
Definition cyclic_stretch.hpp:423
void set_cycle_hz(double hz)
Definition cyclic_stretch.hpp:406
static constexpr double kCrossfadePctDefault
Definition cyclic_stretch.hpp:288
int latency_samples() const noexcept
The initial fill, reported exactly (series law 5).
Definition cyclic_stretch.hpp:483
static constexpr double kOutputDbDefault
Definition cyclic_stretch.hpp:319
static constexpr double kCrossfadeShapeDefault
Definition cyclic_stretch.hpp:292
long long schedule_input_position(long long out_pos) const noexcept
The splice schedule at a given OUTPUT position — no clamping, no buffer state, a pure function of the...
Definition cyclic_stretch.hpp:497
long long grain_count() const noexcept
Number of grains emitted since reset().
Definition cyclic_stretch.hpp:511
double crossfade_shape() const noexcept
Definition cyclic_stretch.hpp:477
long long total_captured() const noexcept
Input samples captured since reset().
Definition cyclic_stretch.hpp:525
static constexpr double kOutputDbMax
Definition cyclic_stretch.hpp:318
long long next_grain_out_pos() const noexcept
The next grain's output position, in absolute output samples.
Definition cyclic_stretch.hpp:514
static constexpr int kGrainPeriodsMin
Definition cyclic_stretch.hpp:278
static constexpr double kStretchRatioMin
Definition cyclic_stretch.hpp:299
long long capture_window_samples() const noexcept
The active capture window in samples — what the falling-behind clamp folds against,...
Definition cyclic_stretch.hpp:473
void set_mix(double percent) noexcept
Definition cyclic_stretch.hpp:439
long long read_position() const noexcept
The live read cursor, in absolute INPUT samples — where the last grain actually read from after the b...
Definition cyclic_stretch.hpp:522
static constexpr int kMinHopSamples
Definition cyclic_stretch.hpp:330
int grain_samples() const noexcept
N — grain length in samples.
Definition cyclic_stretch.hpp:462
static constexpr double kCaptureMsMin
Definition cyclic_stretch.hpp:306
static constexpr double kCycleHzMax
Definition cyclic_stretch.hpp:274
static constexpr double kCrossfadePctMax
Definition cyclic_stretch.hpp:287
double sample_rate() const noexcept
Definition cyclic_stretch.hpp:475
void process(const SampleType *in, SampleType *out, int n)
One block.
Definition cyclic_stretch.hpp:532
The one shared old→new crossfade gain law for the SIGNAL side.
T ms_to_samples(T ms, T sample_rate)
Milliseconds → samples at sample_rate Hz.
Definition units.hpp:147
T db_to_linear(T db)
Amplitude dB → linear gain. 0 dB → 1, −6.0206 dB → 0.5.
Definition units.hpp:71
Definition additive_bank.hpp:218
@ newest
The newest captured frame's phase increment (the original estimate).
Definition freeze_hold.hpp:126
CyclicStretchT< float > CyclicStretch
Definition cyclic_stretch.hpp:853
@ falling
Definition rise_fall_generator.hpp:19
@ rising
Definition rise_fall_generator.hpp:19
long long round_half_even(double x) noexcept
Round half to even, in integer logic.
Definition cyclic_stretch.hpp:227
@ oldest
Recycle the grain furthest through its window — the quietest one when durations match,...
Definition granular.hpp:123
constexpr CyclicStretchRegime kCyclicStretchLongFrame
"Glitchy long frame" — tens of milliseconds per grain, a short linear splice, cycle_hz left wherever ...
Definition cyclic_stretch.hpp:261
T snap_to_zero(T x)
Returns 0 if |x| < snap_threshold, else x.
Definition denormal.hpp:45
@ ratio
base_frequency_hz * ratio.
Definition fm_operator_engine.hpp:20
@ EqualPower
(cos(u·π/2), sin(u·π/2)) — power sum == 1.
Definition crossfade.hpp:43
@ EqualGain
(1−u, u) — amplitude sum == 1.
Definition crossfade.hpp:42
@ u
Definition formant_filter_bank.hpp:20
@ i
Definition formant_filter_bank.hpp:20
CyclicStretchT< double > CyclicStretch64
Definition cyclic_stretch.hpp:854
void crossfade_gains(T u, CrossfadeGainLaw law, T &old_gain, T &new_gain) noexcept
Old/new blend gains for an ALREADY-SHAPED progress u (the caller applies any smoothstep/clamp first).
Definition crossfade.hpp:61
constexpr CyclicStretchRegime kCyclicStretchShortFrame
"Clean-ish short frame" — one cycle per grain, a generous equal-power splice, cycle_hz meant to be tu...
Definition cyclic_stretch.hpp:253
@ fade_out
Definition waveform_editor_primitives.hpp:78
@ fade_in
Definition waveform_editor_primitives.hpp:77
@ k
Definition input_events.hpp:40
One of the two documented sonic regimes.
Definition cyclic_stretch.hpp:240
double crossfade_shape
Definition cyclic_stretch.hpp:244
int grain_periods
Definition cyclic_stretch.hpp:242
double cycle_hz
Definition cyclic_stretch.hpp:241
double crossfade_pct
Definition cyclic_stretch.hpp:243
The real-unit vocabulary shared by every DSP block in the catalog.