Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
granular.hpp
Go to the documentation of this file.
1#pragma once
2
91
95#include <pulp/signal/rng.hpp>
97#include <pulp/signal/units.hpp>
98
99#include <algorithm>
100#include <cmath>
101#include <cstddef>
102#include <cstdint>
103#include <vector>
104
105namespace pulp::signal {
106
109enum class GrainSource : std::uint8_t {
112};
113
116enum class Coherence : std::uint8_t {
119};
120
122enum class StealPolicy : std::uint8_t {
127};
128
132enum class GrainInterp : std::uint8_t {
136};
137
139struct GrainView {
140 bool active = false;
141 double phase = 0.0;
142 double read_position = 0.0;
143 double ratio = 1.0;
144 double gain = 0.0;
145 double pan_left = 0.0;
146 double pan_right = 0.0;
147};
148
149template <typename SampleType = float>
151public:
152 // ── Fixed capacities ──────────────────────────────────────────────────
153
158 static constexpr int kMaxGrainBudget = 64;
159
162 static constexpr int kMinGrainBudget = 32;
163
165 static constexpr int kDefaultMaxGrains = 48;
166
169 static constexpr int kWindowTableSize = 2048;
170
175 static constexpr double kRingSeconds = 4.0;
176
177 // ── Parameter ranges ──────────────────────────────────────────────────
178
180 static constexpr double kDefaultDensityHz = 40.0;
181 static constexpr double kMinDensityHz = 0.1;
182 static constexpr double kMaxDensityHz = 2000.0;
183
187 static constexpr double kDefaultGrainMs = 50.0;
188 static constexpr double kMinGrainMs = 1.0;
189 static constexpr double kMaxGrainMs = 500.0;
190
192 static constexpr double kDefaultJitter = 0.0;
193
195 static constexpr double kMaxPositionSprayMs = 500.0;
196
198 static constexpr double kDefaultStretch = 1.0;
199 static constexpr double kMaxStretch = 4.0;
200
203 static constexpr double kMaxPitchSemitones = 24.0;
204
206 static constexpr double kMaxPitchSpraySemitones = 12.0;
207
209 static constexpr double kMinLevelDb = -24.0;
210 static constexpr double kMaxLevelDb = 12.0;
211
214 static constexpr std::uint32_t kDefaultSeed = 0x9E3779B9u;
215
220 static constexpr double kControlSmoothingMs = 10.0;
221
225 static constexpr double kMinDrawForLog = 1e-12;
226
232 static constexpr double kMinOnsetIntervalSamples = 1.0;
233
234 // ── Keyed-draw field ids ──────────────────────────────────────────────
235 // Distinct ids keep draws that share a grain index from being perfectly
236 // correlated. Values are arbitrary but must never be reused or reordered:
237 // changing one changes every render made with a given seed.
238 static constexpr std::uint64_t kFieldOnsetJitter = 1;
239 static constexpr std::uint64_t kFieldPositionSpray = 2;
240 static constexpr std::uint64_t kFieldPitchSpray = 3;
241 static constexpr std::uint64_t kFieldPanSpray = 4;
242
244 panner_.set_law(PanLaw::EqualPower);
245 rebuild_window();
246 }
247
248 // ── Lifecycle ─────────────────────────────────────────────────────────
249
252 void prepare(double sample_rate) {
253 sample_rate_ = std::isfinite(sample_rate) && sample_rate > 0.0 ? sample_rate
254 : sample_rate_;
255 ring_length_ = static_cast<int>(std::ceil(kRingSeconds * sample_rate_));
256 ring_.assign(static_cast<std::size_t>(ring_length_), SampleType{0});
257 guard_samples_ = derived_guard_samples(sample_rate_);
258 level_.set_ramp_time(kControlSmoothingMs * 0.001, sample_rate_);
259 mix_.set_ramp_time(kControlSmoothingMs * 0.001, sample_rate_);
260 rebuild_window();
261 reset();
262 }
263
268 void reset() {
269 std::fill(ring_.begin(), ring_.end(), SampleType{0});
270 reset_runtime_state();
271 }
272
273private:
279 void audio_fault_reset() {
280 reset_runtime_state();
281 }
282
283 void reset_runtime_state() {
284 for (auto& grain : grains_) grain = Grain{};
285 write_head_ = 0;
286 sample_clock_ = 0;
287 grain_index_ = 0;
288 next_onset_ = 0.0;
289 steal_count_ = 0;
290 clamp_count_ = 0;
291 playhead_ = position01_ * static_cast<double>(std::max(buffer_length_, 1));
292 live_centre_ = 0.0;
293 level_.set_immediate(units::db_to_linear(level_db_));
294 mix_.set_immediate(mix_target_);
295 }
296
297public:
298 // ── Source ────────────────────────────────────────────────────────────
299
300 void set_source(GrainSource source) { source_ = source; }
301 GrainSource source() const { return source_; }
302
305 void set_buffer(const SampleType* data, int length, int channels) {
306 buffer_ = data;
307 buffer_length_ = std::max(length, 0);
308 buffer_channels_ = std::max(channels, 1);
309 playhead_ = position01_ * static_cast<double>(std::max(buffer_length_, 1));
310 }
311
315 void write_live(const SampleType* in, int n) {
316 if (ring_length_ <= 0) return;
317 // Live freeze holds the captured material, not merely its nominal
318 // centre while the ring overwrites it underneath us.
319 if (!accepts_live_input()) return;
320 for (int i = 0; i < n; ++i) {
321 if (!std::isfinite(static_cast<double>(in[i]))) {
322 audio_fault_reset();
323 continue;
324 }
325 write_one(in[i]);
326 }
327 }
328
333 void set_stretch(double rate) {
334 if (!std::isfinite(rate)) return;
335 stretch_ = std::clamp(rate, 0.0, kMaxStretch);
336 }
337 double stretch() const { return stretch_; }
338
342 void set_position(double pos01) {
343 if (!std::isfinite(pos01)) return;
344 position01_ = std::clamp(pos01, 0.0, 1.0);
345 if (source_ == GrainSource::buffer) {
346 playhead_ = position01_ * static_cast<double>(std::max(buffer_length_, 1));
347 }
348 }
349 double position() const { return position01_; }
350
351 void set_position_spray_ms(double ms) {
352 if (!std::isfinite(ms)) return;
353 position_spray_ms_ = std::clamp(ms, 0.0, kMaxPositionSprayMs);
354 }
355 double position_spray_ms() const { return position_spray_ms_; }
356
357 // ── Schedule ──────────────────────────────────────────────────────────
358
359 void set_density_hz(double hz) {
360 if (!std::isfinite(hz)) return;
361 density_hz_ = std::clamp(hz, kMinDensityHz, kMaxDensityHz);
362 }
363 double density_hz() const { return density_hz_; }
364
365 void set_grain_ms(double ms) {
366 if (!std::isfinite(ms)) return;
367 grain_ms_ = std::clamp(ms, kMinGrainMs, kMaxGrainMs);
368 }
369 double grain_ms() const { return grain_ms_; }
370
372 void set_async_jitter(double j01) {
373 if (!std::isfinite(j01)) return;
374 jitter_ = std::clamp(j01, 0.0, 1.0);
375 }
376 double async_jitter() const { return jitter_; }
377
381 void set_max_grains(int count) {
382 const int clamped = std::clamp(count, kMinGrainBudget, kMaxGrainBudget);
383 for (int i = clamped; i < max_grains_; ++i) grains_[static_cast<std::size_t>(i)].active = false;
384 max_grains_ = clamped;
385 }
386 int max_grains() const { return max_grains_; }
387
388 void set_steal_policy(StealPolicy policy) { steal_policy_ = policy; }
389 StealPolicy steal_policy() const { return steal_policy_; }
390
391 // ── Per-grain shaping ─────────────────────────────────────────────────
392
396 void set_window_taper(double t01) {
397 if (!std::isfinite(t01)) return;
398 taper_ = std::clamp(t01, 0.0, 1.0);
399 rebuild_window();
400 }
401 double window_taper() const { return taper_; }
402
403 void set_window_trapezoid(bool trapezoid) {
404 trapezoid_ = trapezoid;
405 rebuild_window();
406 }
407 bool window_trapezoid() const { return trapezoid_; }
408
409 void set_pitch_semitones(double st) {
410 if (!std::isfinite(st)) return;
411 pitch_st_ = std::clamp(st, -kMaxPitchSemitones, kMaxPitchSemitones);
412 }
413 double pitch_semitones() const { return pitch_st_; }
414
416 if (!std::isfinite(st)) return;
417 pitch_spray_st_ = std::clamp(st, 0.0, kMaxPitchSpraySemitones);
418 }
419
420 void set_pan_spray(double s01) {
421 if (!std::isfinite(s01)) return;
422 pan_spray_ = std::clamp(s01, 0.0, 1.0);
423 }
424
426 Coherence coherence() const { return coherence_; }
427
428 void set_interp(GrainInterp interp) { interp_ = interp; }
429 GrainInterp interp() const { return interp_; }
430
431 void set_level_db(double db) {
432 if (!std::isfinite(db)) return;
433 level_db_ = std::clamp(db, kMinLevelDb, kMaxLevelDb);
434 level_.set_target(units::db_to_linear(level_db_));
435 }
436
440 void set_mix(double mix01) {
441 if (!std::isfinite(mix01)) return;
442 mix_target_ = std::clamp(mix01, 0.0, 1.0);
443 mix_.set_target(mix_target_);
444 }
445 double mix() const { return mix_target_; }
446
448 void set_seed(std::uint32_t seed) { seed_ = seed; }
449 std::uint32_t seed() const { return seed_; }
450
451 // ── Queries ───────────────────────────────────────────────────────────
452
455 std::size_t latency_samples() const { return 0; }
456
459 double mean_overlap() const { return density_hz_ * grain_ms_ * 0.001; }
460
462 double grain_gain() const {
463 const double overlap = std::max(mean_overlap(), 1e-9);
464 const double raw = coherence_ == Coherence::incoherent
465 ? 1.0 / (window_rms_ * std::sqrt(overlap))
466 : 1.0 / (window_mean_ * overlap);
467 return std::min(1.0, raw);
468 }
469
472 double window_mean() const { return window_mean_; }
473 double window_rms() const { return window_rms_; }
474
477 double window_at(double phase) const {
478 const double x = std::clamp(phase, 0.0, 1.0) * static_cast<double>(kWindowTableSize);
479 int index = static_cast<int>(x);
480 if (index >= kWindowTableSize) index = kWindowTableSize - 1;
481 const double frac = x - static_cast<double>(index);
482 const double a = window_[static_cast<std::size_t>(index)];
483 const double b = window_[static_cast<std::size_t>(index) + 1];
484 return a + frac * (b - a);
485 }
486
487 int active_grain_count() const {
488 int count = 0;
489 for (int i = 0; i < max_grains_; ++i) {
490 if (grains_[static_cast<std::size_t>(i)].active) ++count;
491 }
492 return count;
493 }
494
495 GrainView grain(int slot) const {
497 if (slot < 0 || slot >= kMaxGrainBudget) return view;
498 const Grain& g = grains_[static_cast<std::size_t>(slot)];
499 view.active = g.active;
500 view.phase = g.phase;
501 view.read_position = g.read_position;
502 view.ratio = g.ratio;
503 view.gain = g.gain;
504 view.pan_left = g.pan_left;
505 view.pan_right = g.pan_right;
506 return view;
507 }
508
510 std::uint64_t grain_index() const { return grain_index_; }
511
513 std::uint64_t steal_count() const { return steal_count_; }
514
516 std::uint64_t clamp_count() const { return clamp_count_; }
517
518 int ring_length() const { return ring_length_; }
519
523 SampleType ring_storage_sample(int index) const {
524 if (index < 0 || index >= ring_length_) return SampleType{0};
525 return ring_[static_cast<std::size_t>(index)];
526 }
527
532 int causality_guard_samples() const { return guard_samples_; }
533
534 static int derived_guard_samples(double sample_rate) {
535 const double r_max = std::exp2(kMaxPitchSemitones / 12.0);
536 return static_cast<int>(
537 std::ceil(sample_rate * (kMaxPositionSprayMs * 0.001 + kMaxGrainMs * 0.001 * r_max)));
538 }
539
540 // ── Render ────────────────────────────────────────────────────────────
541
544 void process(SampleType* out_left, SampleType* out_right, int n) {
545 render(nullptr, out_left, out_right, n);
546 }
547
558 void process(const SampleType* in, SampleType* out_left, SampleType* out_right, int n) {
559 render(in, out_left, out_right, n);
560 }
561
562private:
563 bool accepts_live_input() const {
564 return source_ != GrainSource::live_ring || stretch_ != 0.0;
565 }
566
567 struct Grain {
568 bool active = false;
569 double phase = 0.0;
570 double phase_increment = 0.0;
571 double read_position = 0.0;
572 double ratio = 1.0;
573 double gain = 0.0;
574 double pan_left = 0.0;
575 double pan_right = 0.0;
576 };
577
578 static constexpr double kPi = 3.14159265358979323846;
579
580 // ── Window table ──────────────────────────────────────────────────────
581
586 void rebuild_window() {
587 window_.resize(static_cast<std::size_t>(kWindowTableSize) + 1);
588 for (int i = 0; i <= kWindowTableSize; ++i) {
589 window_[static_cast<std::size_t>(i)] =
590 window_shape(static_cast<double>(i) / static_cast<double>(kWindowTableSize));
591 }
592 // Mean and RMS over the half-open phase domain [0, 1): the last table
593 // entry is the wrap guard for interpolation and would double-count the
594 // p = 0 point.
595 double sum = 0.0;
596 double sum_squares = 0.0;
597 for (int i = 0; i < kWindowTableSize; ++i) {
598 const double w = window_[static_cast<std::size_t>(i)];
599 sum += w;
600 sum_squares += w * w;
601 }
602 window_mean_ = sum / static_cast<double>(kWindowTableSize);
603 window_rms_ = std::sqrt(sum_squares / static_cast<double>(kWindowTableSize));
604 }
605
606 double window_shape(double p) const {
607 if (taper_ <= 0.0) return 1.0;
608 const double half = 0.5 * taper_;
609 const double edge = p < half ? p : (p > 1.0 - half ? 1.0 - p : half);
610 if (edge >= half) return 1.0;
611 const double t = edge / half; // 0 at the grain edge, 1 where the flat top starts
612 if (trapezoid_) return t;
613 // Cosine taper. At taper = 1 the two halves meet at p = 0.5 and this is
614 // exactly Hann: 0.5·(1 − cos(2πp)) = sin²(πp).
615 return 0.5 * (1.0 - std::cos(kPi * t));
616 }
617
618 // ── Schedule ──────────────────────────────────────────────────────────
619
630 double onset_interval_samples(std::uint64_t k) const {
631 const double h = sample_rate_ / density_hz_;
632 if (jitter_ <= 0.0) return h;
633 double u = unit_from<double>(mix64(seed_, k, kFieldOnsetJitter));
635 const double exponential = -std::log(u) * h;
636 const double blended = (1.0 - jitter_) * h + jitter_ * exponential;
637 return std::max(blended, kMinOnsetIntervalSamples);
638 }
639
640 int allocate_slot() {
641 for (int i = 0; i < max_grains_; ++i) {
642 if (!grains_[static_cast<std::size_t>(i)].active) return i;
643 }
644 if (steal_policy_ == StealPolicy::drop_new) return -1;
645
646 int best = 0;
647 if (steal_policy_ == StealPolicy::oldest) {
648 // Furthest through its window. With equal durations that is also the
649 // quietest, which is why stealing is click-free.
650 double best_phase = -1.0;
651 for (int i = 0; i < max_grains_; ++i) {
652 const double phase = grains_[static_cast<std::size_t>(i)].phase;
653 if (phase > best_phase) {
654 best_phase = phase;
655 best = i;
656 }
657 }
658 } else {
659 double best_level = 1e300;
660 for (int i = 0; i < max_grains_; ++i) {
661 const Grain& g = grains_[static_cast<std::size_t>(i)];
662 const double level = window_at(g.phase) * g.gain;
663 if (level < best_level) {
664 best_level = level;
665 best = i;
666 }
667 }
668 }
669 ++steal_count_;
670 return best;
671 }
672
673 void spawn(std::uint64_t k) {
674 const double u_position = unit_from<double>(mix64(seed_, k, kFieldPositionSpray));
675 const double u_pitch = unit_from<double>(mix64(seed_, k, kFieldPitchSpray));
676 const double u_pan = unit_from<double>(mix64(seed_, k, kFieldPanSpray));
677
678 const double semitones = pitch_st_ + (u_pitch * 2.0 - 1.0) * pitch_spray_st_;
679 Grain next{};
680 next.ratio = units::semitones_to_ratio(semitones);
681
682 const double grain_seconds = grain_ms_ * 0.001;
683 next.phase = 0.0;
684 next.phase_increment = 1.0 / std::max(grain_seconds * sample_rate_, 1.0);
685 next.gain = grain_gain();
686
687 const double spray =
688 (u_position * 2.0 - 1.0) * units::ms_to_samples(position_spray_ms_, sample_rate_);
689 const double requested_read = source_ == GrainSource::buffer ? playhead_ + spray
690 : live_centre_ - spray;
691 if (source_ == GrainSource::buffer) {
692 next.read_position = requested_read;
693 } else if (!place_live_read(requested_read, next.ratio, grain_seconds,
694 next.read_position)) {
695 // Nothing valid to read yet — the ring has not captured enough past.
696 // The grain is skipped rather than placed on unwritten samples;
697 // `grain_index_` still advances, so no draw shifts and determinism
698 // is untouched.
699 return;
700 }
701
702 // Placement is known-good before a sounding slot can be selected. A
703 // failed live birth therefore cannot partially overwrite a victim or
704 // count as a steal.
705 const int slot = allocate_slot();
706 if (slot < 0) return;
707 if (next.read_position != requested_read) ++clamp_count_;
708
709 panner_.set_pan(std::clamp((u_pan * 2.0 - 1.0) * pan_spray_, -1.0, 1.0));
710 panner_.compute_gains(next.pan_left, next.pan_right);
711
712 next.active = true;
713 grains_[static_cast<std::size_t>(slot)] = next;
714 }
715
741 bool place_live_read(double start, double ratio, double grain_seconds, double& out) {
742 const double duration = grain_seconds * sample_rate_;
743 const double head = static_cast<double>(write_head_);
744 const double newest = head - std::max(0.0, ratio - 1.0) * duration - 2.0;
745 const double oldest =
746 std::max(0.0, head - static_cast<double>(ring_length_) +
747 std::max(0.0, 1.0 - ratio) * duration) +
748 2.0;
749 if (newest < oldest) return false;
750 out = std::clamp(start, oldest, newest);
751 return true;
752 }
753
754 // ── Source reads ──────────────────────────────────────────────────────
755
756 void write_one(SampleType x) {
757 ring_[static_cast<std::size_t>(write_head_ % ring_length_)] = x;
758 ++write_head_;
759 }
760
763 static int wrap_index(std::int64_t index, int length) {
764 std::int64_t m = index % length;
765 if (m < 0) m += length;
766 return static_cast<int>(m);
767 }
768
769 double buffer_frame(std::int64_t frame) const {
770 if (buffer_ == nullptr || buffer_length_ <= 0) return 0.0;
771 const int index = wrap_index(frame, buffer_length_);
772 // Multi-channel folds to mono as the channel MEAN, not the sum, so a
773 // stereo source does not arrive 6 dB hotter than a mono one and the
774 // normalization law stays valid for both.
775 double sum = 0.0;
776 for (int c = 0; c < buffer_channels_; ++c) {
777 const double sample = static_cast<double>(
778 buffer_[static_cast<std::size_t>(index) * static_cast<std::size_t>(buffer_channels_) +
779 static_cast<std::size_t>(c)]);
780 if (!std::isfinite(sample)) return 0.0;
781 sum += sample;
782 }
783 return sum / static_cast<double>(buffer_channels_);
784 }
785
786 double ring_sample(std::int64_t index) const {
787 if (ring_length_ <= 0) return 0.0;
788 return static_cast<double>(ring_[static_cast<std::size_t>(wrap_index(index, ring_length_))]);
789 }
790
791 double read_source(double position) const {
792 const double floored = std::floor(position);
793 const auto base = static_cast<std::int64_t>(floored);
794 const double t = position - floored;
795
796 double y0 = 0.0;
797 double y1 = 0.0;
798 if (source_ == GrainSource::buffer) {
799 y0 = buffer_frame(base);
800 y1 = buffer_frame(base + 1);
801 } else {
802 y0 = ring_sample(base);
803 y1 = ring_sample(base + 1);
804 }
805 if (interp_ == GrainInterp::linear) return Interpolator::linear(t, y0, y1);
806
807 const double ym1 =
808 source_ == GrainSource::buffer ? buffer_frame(base - 1) : ring_sample(base - 1);
809 const double y2 =
810 source_ == GrainSource::buffer ? buffer_frame(base + 2) : ring_sample(base + 2);
811 // Catmull-Rom, from the shared interpolator rather than a second copy of
812 // the same four coefficients.
813 return Interpolator::hermite(t, ym1, y0, y1, y2);
814 }
815
816 // ── Render core ───────────────────────────────────────────────────────
817
818 void render(const SampleType* in, SampleType* out_left, SampleType* out_right, int n) {
819 const double buffer_span = static_cast<double>(std::max(buffer_length_, 1));
820 for (int i = 0; i < n; ++i) {
821 const double input_sample = in != nullptr ? static_cast<double>(in[i]) : 0.0;
822 if (in != nullptr && !std::isfinite(input_sample)) {
823 // Live input is ring history and the interleaved overload also
824 // exposes it as dry audio. Reject it before either path sees it,
825 // then restart from the documented fresh state.
826 audio_fault_reset();
827 out_left[i] = SampleType{0};
828 out_right[i] = SampleType{0};
829 continue;
830 }
831 if (in != nullptr && accepts_live_input()) {
832 write_one(in[i]);
833 }
834
835 if (source_ == GrainSource::live_ring && stretch_ > 0.0) {
836 live_centre_ = static_cast<double>(write_head_) -
837 position01_ * static_cast<double>(ring_length_);
838 }
839
840 // Spawn every grain whose onset has arrived. Driven by the absolute
841 // sample clock, so where the block boundaries fall changes nothing.
842 while (next_onset_ <= static_cast<double>(sample_clock_)) {
843 spawn(grain_index_);
844 next_onset_ += onset_interval_samples(grain_index_);
845 ++grain_index_;
846 }
847
848 double left = 0.0;
849 double right = 0.0;
850 for (int s = 0; s < max_grains_; ++s) {
851 Grain& g = grains_[static_cast<std::size_t>(s)];
852 if (!g.active) continue;
853 const double y = read_source(g.read_position) * window_at(g.phase) * g.gain;
854 left += y * g.pan_left;
855 right += y * g.pan_right;
856 g.read_position += g.ratio;
857 g.phase += g.phase_increment;
858 if (g.phase >= 1.0) g.active = false;
859 }
860
861 const double level = level_.next();
862 const double blend = mix_.next();
863 const double dry = input_sample;
864 const double wet_weight = in != nullptr ? blend : 1.0;
865 const double dry_weight = in != nullptr ? 1.0 - blend : 0.0;
866
867 out_left[i] = static_cast<SampleType>(
868 snap_to_zero(left * level * wet_weight + dry * dry_weight));
869 out_right[i] = static_cast<SampleType>(
870 snap_to_zero(right * level * wet_weight + dry * dry_weight));
871
872 if (source_ == GrainSource::buffer) {
873 playhead_ += stretch_;
874 if (playhead_ >= buffer_span) playhead_ -= buffer_span;
875 if (playhead_ < 0.0) playhead_ += buffer_span;
876 }
877 ++sample_clock_;
878 }
879 }
880
881 // ── State ─────────────────────────────────────────────────────────────
882
883 double sample_rate_ = 44100.0;
884
886 const SampleType* buffer_ = nullptr;
887 int buffer_length_ = 0;
888 int buffer_channels_ = 1;
889
890 std::vector<SampleType> ring_{};
891 int ring_length_ = 0;
892 int guard_samples_ = 0;
893 std::int64_t write_head_ = 0;
894 double live_centre_ = 0.0;
895
896 double density_hz_ = kDefaultDensityHz;
897 double grain_ms_ = kDefaultGrainMs;
898 double jitter_ = kDefaultJitter;
899 double stretch_ = kDefaultStretch;
900 double position01_ = 0.0;
901 double position_spray_ms_ = 0.0;
902 double pitch_st_ = 0.0;
903 double pitch_spray_st_ = 0.0;
904 double pan_spray_ = 0.0;
905 double taper_ = 0.5;
906 bool trapezoid_ = false;
907 double level_db_ = 0.0;
908 double mix_target_ = 1.0;
909
910 Coherence coherence_ = Coherence::incoherent;
911 StealPolicy steal_policy_ = StealPolicy::oldest;
913 int max_grains_ = kDefaultMaxGrains;
914 std::uint32_t seed_ = kDefaultSeed;
915
916 std::vector<double> window_{};
917 double window_mean_ = 1.0;
918 double window_rms_ = 1.0;
919
920 Grain grains_[kMaxGrainBudget]{};
921 double playhead_ = 0.0;
922 double next_onset_ = 0.0;
923 std::int64_t sample_clock_ = 0;
924 std::uint64_t grain_index_ = 0;
925 std::uint64_t steal_count_ = 0;
926 std::uint64_t clamp_count_ = 0;
927
928 SmoothedValue<double> level_{1.0};
929 SmoothedValue<double> mix_{1.0};
930 PannerT<double> panner_{};
931};
932
935
936} // namespace pulp::signal
Definition granular.hpp:150
static constexpr double kDefaultDensityHz
Definition granular.hpp:180
std::uint64_t grain_index() const
Grains born since reset(). Monotonic; this is the key every draw uses.
Definition granular.hpp:510
double window_taper() const
Definition granular.hpp:401
GrainInterp interp() const
Definition granular.hpp:429
void set_source(GrainSource source)
Definition granular.hpp:300
double window_at(double phase) const
Window value at a normalised grain phase — the same table read the audio path uses,...
Definition granular.hpp:477
void set_grain_ms(double ms)
Definition granular.hpp:365
void set_position(double pos01)
Playhead centre as a fraction of the source.
Definition granular.hpp:342
static constexpr double kRingSeconds
Definition granular.hpp:175
void set_max_grains(int count)
Grains in slots above the new budget stop immediately, so lowering the budget cannot leave a grain so...
Definition granular.hpp:381
static constexpr int kMaxGrainBudget
Definition granular.hpp:158
static constexpr std::uint64_t kFieldPitchSpray
Definition granular.hpp:240
void set_pan_spray(double s01)
Definition granular.hpp:420
StealPolicy steal_policy() const
Definition granular.hpp:389
Coherence coherence() const
Definition granular.hpp:426
double position_spray_ms() const
Definition granular.hpp:355
void set_coherence(Coherence coherence)
Definition granular.hpp:425
double grain_gain() const
Per-grain gain the next spawn will use, from the current law and params.
Definition granular.hpp:462
static constexpr int kMinGrainBudget
Definition granular.hpp:162
static constexpr std::uint64_t kFieldOnsetJitter
Definition granular.hpp:238
int causality_guard_samples() const
The derived causality guard, in samples: the worst-case total displacement a grain's read pointer can...
Definition granular.hpp:532
double pitch_semitones() const
Definition granular.hpp:413
static constexpr std::uint64_t kFieldPanSpray
Definition granular.hpp:241
static constexpr int kDefaultMaxGrains
Definition granular.hpp:165
void set_position_spray_ms(double ms)
Definition granular.hpp:351
void set_buffer(const SampleType *data, int length, int channels)
Borrows an external interleaved buffer.
Definition granular.hpp:305
std::uint64_t clamp_count() const
Spawns whose read position the causality clamp had to move.
Definition granular.hpp:516
static constexpr double kMaxDensityHz
Definition granular.hpp:182
static constexpr double kMaxPitchSpraySemitones
Definition granular.hpp:206
static constexpr double kDefaultStretch
Definition granular.hpp:198
void set_pitch_semitones(double st)
Definition granular.hpp:409
static constexpr double kMaxPitchSemitones
Definition granular.hpp:203
void write_live(const SampleType *in, int n)
Pushes n mono samples into the live ring.
Definition granular.hpp:315
GrainSource source() const
Definition granular.hpp:301
void process(const SampleType *in, SampleType *out_left, SampleType *out_right, int n)
Live render that consumes its own input, one sample at a time.
Definition granular.hpp:558
static constexpr double kDefaultJitter
Definition granular.hpp:192
static constexpr double kMinLevelDb
Definition granular.hpp:209
GrainView grain(int slot) const
Definition granular.hpp:495
double window_mean() const
Measured from the shipped table, not assumed.
Definition granular.hpp:472
double grain_ms() const
Definition granular.hpp:369
static constexpr double kMaxGrainMs
Definition granular.hpp:189
void set_stretch(double rate)
Playhead advance multiplier.
Definition granular.hpp:333
int ring_length() const
Definition granular.hpp:518
void set_mix(double mix01)
Dry/wet, 0 = dry .
Definition granular.hpp:440
int active_grain_count() const
Definition granular.hpp:487
bool window_trapezoid() const
Definition granular.hpp:407
void set_steal_policy(StealPolicy policy)
Definition granular.hpp:388
void prepare(double sample_rate)
Sizes the live ring and the window table.
Definition granular.hpp:252
static constexpr int kWindowTableSize
Definition granular.hpp:169
void reset()
Clears grains and the ring, rewinds the schedule, and restarts the random stream.
Definition granular.hpp:268
static constexpr double kMinDensityHz
Definition granular.hpp:181
double mean_overlap() const
Mean simultaneous overlap N̄ = D·T_g — the quantity both normalization laws are written in terms of.
Definition granular.hpp:459
static int derived_guard_samples(double sample_rate)
Definition granular.hpp:534
static constexpr double kMinGrainMs
Definition granular.hpp:188
static constexpr double kMaxStretch
Definition granular.hpp:199
static constexpr double kDefaultGrainMs
Definition granular.hpp:187
void set_window_taper(double t01)
0 is rectangular, 1 is Hann (cosine mode) or triangle (trapezoid mode); in between is a Tukey / flat-...
Definition granular.hpp:396
void process(SampleType *out_left, SampleType *out_right, int n)
Pure-source render.
Definition granular.hpp:544
static constexpr std::uint32_t kDefaultSeed
Definition granular.hpp:214
static constexpr double kControlSmoothingMs
Definition granular.hpp:220
static constexpr double kMinOnsetIntervalSamples
Definition granular.hpp:232
void set_density_hz(double hz)
Definition granular.hpp:359
double window_rms() const
Definition granular.hpp:473
static constexpr double kMaxPositionSprayMs
Definition granular.hpp:195
int max_grains() const
Definition granular.hpp:386
double async_jitter() const
Definition granular.hpp:376
void set_async_jitter(double j01)
0 is the exact clock; 1 is a true Poisson process.
Definition granular.hpp:372
std::size_t latency_samples() const
Zero.
Definition granular.hpp:455
void set_seed(std::uint32_t seed)
Fixed at reset(); never automatable (series law 2).
Definition granular.hpp:448
void set_interp(GrainInterp interp)
Definition granular.hpp:428
std::uint32_t seed() const
Definition granular.hpp:449
static constexpr std::uint64_t kFieldPositionSpray
Definition granular.hpp:239
void set_level_db(double db)
Definition granular.hpp:431
static constexpr double kMinDrawForLog
Definition granular.hpp:225
static constexpr double kMaxLevelDb
Definition granular.hpp:210
SampleType ring_storage_sample(int index) const
Physical live-ring storage for diagnostics.
Definition granular.hpp:523
GranularEngineT()
Definition granular.hpp:243
void set_window_trapezoid(bool trapezoid)
Definition granular.hpp:403
double stretch() const
Definition granular.hpp:337
double mix() const
Definition granular.hpp:445
void set_pitch_spray_semitones(double st)
Definition granular.hpp:415
std::uint64_t steal_count() const
Grains recycled out from under a still-sounding predecessor.
Definition granular.hpp:513
double density_hz() const
Definition granular.hpp:363
double position() const
Definition granular.hpp:349
void set_immediate(T value)
Definition smoothed_value.hpp:71
High-quality sample interpolation for delay lines and resampling.
@ g
Definition pitch.hpp:27
@ index
Definition gpu_pass.hpp:140
T semitones_to_ratio(T semitones)
Semitones → frequency ratio. +12 → 2, −12 → 0.5.
Definition units.hpp:105
T ms_to_samples(T ms, T sample_rate)
Milliseconds → samples at sample_rate Hz.
Definition units.hpp:147
@ half
Definition units.hpp:290
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
GrainSource
Where grains read from.
Definition granular.hpp:109
@ buffer
A borrowed static buffer with a traversable playhead.
Definition granular.hpp:110
@ live_ring
A continuously captured ring of recent input.
Definition granular.hpp:111
@ newest
The newest captured frame's phase increment (the original estimate).
Definition freeze_hold.hpp:126
@ right
Definition multi_channel_meter.hpp:67
@ left
Definition multi_channel_meter.hpp:66
@ exponential
Definition mod_tools.hpp:106
@ c
Definition spectrum_trace.hpp:35
constexpr std::uint64_t mix64(std::uint64_t z)
splitmix64 finalizer (Steele, Lea & Flood, OOPSLA 2014).
Definition rng.hpp:140
StealPolicy
What happens when a grain is due and the pool is full.
Definition granular.hpp:122
@ drop_new
Skip the incoming grain. Thins clouds rather than cutting.
Definition granular.hpp:126
@ oldest
Recycle the grain furthest through its window — the quietest one when durations match,...
Definition granular.hpp:123
@ quietest
Recycle the smallest current w(phase)·g. For mixed lengths.
Definition granular.hpp:125
@ EqualPower
Definition panner.hpp:25
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
@ a
Definition formant_filter_bank.hpp:20
@ u
Definition formant_filter_bank.hpp:20
@ i
Definition formant_filter_bank.hpp:20
@ e
Definition formant_filter_bank.hpp:20
Coherence
Which normalization law applies — a statement about whether grains are correlated with each other,...
Definition granular.hpp:116
@ incoherent
Scattered grains; powers add; 1/(w_rms·√N̄).
Definition granular.hpp:117
@ coherent
Grid-aligned grains; amplitudes add; 1/(w_mean·N̄).
Definition granular.hpp:118
GrainInterp
Fractional-read quality inside a grain.
Definition granular.hpp:132
@ cubic_hermite
4-point Catmull-Rom.
Definition granular.hpp:134
@ linear
2-point. Cheapest; audible imaging when pitching up.
Definition granular.hpp:133
GranularEngineT< float > GranularEngine
Definition granular.hpp:933
GranularEngineT< double > GranularEngine64
Definition granular.hpp:934
float unit_from(std::uint64_t key)
Stateless purpose-keyed draw in [0, 1).
Definition rng.hpp:164
float sum(const float *data, std::size_t count) noexcept
float sum_squares(const float *data, std::size_t count) noexcept
Hover-cursor tracking for platform window hosts.
Definition fwd.hpp:27
@ s
Definition input_events.hpp:41
@ h
Definition input_events.hpp:39
@ y
Definition input_events.hpp:42
@ m
Definition input_events.hpp:40
@ n
Definition input_events.hpp:40
@ p
Definition input_events.hpp:40
@ t
Definition input_events.hpp:41
@ w
Definition input_events.hpp:41
Deterministic randomness for modulation: a counter-free PRNG, a Gaussian draw, a stateless purpose-ke...
A read-only view of one grain, for metering and grain-cloud displays.
Definition granular.hpp:139
double pan_right
Definition granular.hpp:146
double phase
Definition granular.hpp:141
double ratio
Definition granular.hpp:143
double read_position
Definition granular.hpp:142
double pan_left
Definition granular.hpp:145
double gain
Definition granular.hpp:144
bool active
Definition granular.hpp:140
static SampleType linear(SampleType frac, SampleType y0, SampleType y1)
Linear interpolation between two samples.
Definition interpolator.hpp:33
static SampleType hermite(SampleType frac, SampleType ym1, SampleType y0, SampleType y1, SampleType y2)
Cubic Hermite (Catmull-Rom) interpolation.
Definition interpolator.hpp:41
The real-unit vocabulary shared by every DSP block in the catalog.