Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
particle_collision_exciter.hpp
Go to the documentation of this file.
1#pragma once
2
4#include <pulp/signal/rng.hpp>
5
6#include <algorithm>
7#include <cmath>
8#include <cstdint>
9#include <limits>
10#include <type_traits>
11
12namespace pulp::signal {
13
23template <typename SampleType = float>
25 static_assert(std::is_floating_point_v<SampleType>);
26
27public:
28 static constexpr int kMaxParticleCount = 2000;
29 static constexpr double kDefaultSampleRate = 48000.0;
30 static constexpr double kMinSupportedSampleRate = 8000.0;
31 static constexpr std::uint64_t kDefaultSeed = 0x5041525449434C45ull;
32
34 bool occurred = false;
35 SampleType amplitude{};
36 };
37
39 apply_seed();
40 update_decay_coefficient();
41 reset();
42 }
43
47 void prepare(double sample_rate) {
48 if (std::isfinite(sample_rate) && sample_rate >= kMinSupportedSampleRate)
49 sample_rate_ = sample_rate;
50 event_rng_.prepare(sample_rate_);
51 jitter_rng_.prepare(sample_rate_);
52 update_decay_coefficient();
53 reset();
54 }
55
56 void set_seed(std::uint64_t seed) noexcept {
57 seed_ = seed;
58 apply_seed();
59 reset();
60 }
61
62 void set_particle_count(int count) noexcept {
63 particle_count_ = std::clamp(count, 1, kMaxParticleCount);
64 particle_scale_ = std::sqrt(static_cast<double>(particle_count_));
65 }
66
67 void set_event_rate(SampleType events_per_second) noexcept {
68 const double value = static_cast<double>(events_per_second);
69 if (std::isfinite(value))
70 event_rate_ = std::clamp(value, 1.0, 20000.0);
71 }
72
73 void set_energy_decay(SampleType milliseconds) noexcept {
74 const double value = static_cast<double>(milliseconds);
75 if (!std::isfinite(value))
76 return;
77 energy_decay_ms_ = std::clamp(value, 5.0, 4000.0);
78 update_decay_coefficient();
79 }
80
81 void set_sustain_floor(SampleType floor) noexcept {
82 const double value = static_cast<double>(floor);
83 if (!std::isfinite(value))
84 return;
85 sustain_floor_ = std::clamp(value, 0.0, 0.2);
86 snap_energy();
87 }
88
90 void set_collision_energy_loss(SampleType loss) noexcept {
91 const double value = static_cast<double>(loss);
92 if (std::isfinite(value))
93 collision_energy_loss_ = std::clamp(value, 0.0001, 0.05);
94 }
95
96 void reset() noexcept {
97 energy_ = 0.0;
98 event_rng_.reset();
99 jitter_rng_.reset();
100 }
101
102 void excite(SampleType amount01) noexcept {
103 const double amount = static_cast<double>(amount01);
104 if (std::isfinite(amount) && amount > 0.0)
105 energy_ = std::clamp(energy_ + amount, 0.0, 1.0);
106 }
107
108 CollisionEvent next() noexcept {
109 energy_ *= decay_coefficient_;
110 snap_energy();
111
112 // NoiseSourceT::white() is uniform on [-1, 1), hence this is [0, 1).
113 const double event_draw = 0.5 * (static_cast<double>(event_rng_.white()) + 1.0);
114 const double lambda_dt = event_rate_ * particle_scale_ * energy_ / sample_rate_;
115 const double probability = -std::expm1(-lambda_dt);
116 if (!(event_draw < probability))
117 return {};
118
119 const double pre_loss_energy = energy_;
120 const double jitter = 0.5 * (static_cast<double>(jitter_rng_.white()) + 1.0);
121 const double amplitude = pre_loss_energy * (0.5 + 0.5 * jitter);
122 SampleType converted_amplitude = static_cast<SampleType>(amplitude);
123 if (!(static_cast<double>(converted_amplitude) < pre_loss_energy)) {
124 converted_amplitude = std::nextafter(
125 static_cast<SampleType>(pre_loss_energy),
126 std::numeric_limits<SampleType>::lowest());
127 }
128
129 energy_ = std::max(0.0, energy_ - collision_energy_loss_);
130 snap_energy();
131 return {true, converted_amplitude};
132 }
133
134 [[nodiscard]] SampleType energy() const noexcept {
135 return static_cast<SampleType>(energy_);
136 }
137 [[nodiscard]] int particle_count() const noexcept { return particle_count_; }
138 [[nodiscard]] SampleType event_rate() const noexcept {
139 return static_cast<SampleType>(event_rate_);
140 }
141 [[nodiscard]] SampleType energy_decay_ms() const noexcept {
142 return static_cast<SampleType>(energy_decay_ms_);
143 }
144 [[nodiscard]] SampleType sustain_floor() const noexcept {
145 return static_cast<SampleType>(sustain_floor_);
146 }
147 [[nodiscard]] SampleType collision_energy_loss() const noexcept {
148 return static_cast<SampleType>(collision_energy_loss_);
149 }
150 [[nodiscard]] std::uint64_t seed() const noexcept { return seed_; }
151 [[nodiscard]] double sample_rate() const noexcept { return sample_rate_; }
152
153 static constexpr int latency_samples() noexcept { return 0; }
154 static constexpr std::size_t retained_bytes() noexcept { return 0; }
155
158 [[nodiscard]] int tail_samples() const noexcept {
159 if (energy_ <= 0.0)
160 return 0;
161 if (sustain_floor_ <= 0.0 || !(decay_coefficient_ > 0.0 && decay_coefficient_ < 1.0))
162 return -1;
163 if (energy_ < sustain_floor_)
164 return 0;
165 const double samples = std::ceil(std::log(sustain_floor_ / energy_) /
166 std::log(decay_coefficient_));
167 if (!std::isfinite(samples) ||
168 samples >= static_cast<double>(std::numeric_limits<int>::max()))
169 return std::numeric_limits<int>::max();
170 return std::max(0, static_cast<int>(samples) + 1);
171 }
172
173private:
174 static std::uint32_t derive_seed(std::uint64_t seed, std::uint64_t purpose) noexcept {
175 const std::uint64_t mixed = mix64(seed, 0u, purpose);
176 const auto folded = static_cast<std::uint32_t>(mixed ^ (mixed >> 32));
177 return folded == 0u ? NoiseSourceT<double>::default_seed : folded;
178 }
179
180 void apply_seed() noexcept {
181 event_rng_.set_seed(derive_seed(seed_, 0x4556454E54ull));
182 jitter_rng_.set_seed(derive_seed(seed_, 0x4A4954544552ull));
183 }
184
185 void update_decay_coefficient() noexcept {
186 decay_coefficient_ = std::exp(-1.0 / (0.001 * energy_decay_ms_ * sample_rate_));
187 }
188
189 void snap_energy() noexcept {
190 if (energy_ < sustain_floor_)
191 energy_ = 0.0;
192 }
193
194 NoiseSourceT<double> event_rng_;
195 NoiseSourceT<double> jitter_rng_;
196 double sample_rate_ = kDefaultSampleRate;
197 double energy_ = 0.0;
198 double event_rate_ = 400.0;
199 double energy_decay_ms_ = 250.0;
200 double sustain_floor_ = 0.02;
201 double collision_energy_loss_ = 0.004;
202 double particle_scale_ = 8.0;
203 double decay_coefficient_ = 0.0;
204 int particle_count_ = 64;
205 std::uint64_t seed_ = kDefaultSeed;
206};
207
210
211} // namespace pulp::signal
void set_seed(std::uint32_t seed)
Sets the seed used by reset().
Definition noise_source.hpp:96
static constexpr std::uint32_t default_seed
Default seed.
Definition noise_source.hpp:55
Bounded stochastic collision model for shaken particle instruments.
Definition particle_collision_exciter.hpp:24
static constexpr double kMinSupportedSampleRate
Definition particle_collision_exciter.hpp:30
void reset() noexcept
Definition particle_collision_exciter.hpp:96
void prepare(double sample_rate)
Reconfigures the two NoiseSourceT streams and clears dynamic state.
Definition particle_collision_exciter.hpp:47
void set_sustain_floor(SampleType floor) noexcept
Definition particle_collision_exciter.hpp:81
void set_particle_count(int count) noexcept
Definition particle_collision_exciter.hpp:62
SampleType energy() const noexcept
Definition particle_collision_exciter.hpp:134
double sample_rate() const noexcept
Definition particle_collision_exciter.hpp:151
void set_seed(std::uint64_t seed) noexcept
Definition particle_collision_exciter.hpp:56
SampleType event_rate() const noexcept
Definition particle_collision_exciter.hpp:138
ParticleCollisionExciterT()
Definition particle_collision_exciter.hpp:38
static constexpr double kDefaultSampleRate
Definition particle_collision_exciter.hpp:29
CollisionEvent next() noexcept
Definition particle_collision_exciter.hpp:108
std::uint64_t seed() const noexcept
Definition particle_collision_exciter.hpp:150
SampleType collision_energy_loss() const noexcept
Definition particle_collision_exciter.hpp:147
SampleType energy_decay_ms() const noexcept
Definition particle_collision_exciter.hpp:141
int tail_samples() const noexcept
Conservative samples until the decay envelope reaches its exact floor, assuming no more excite() call...
Definition particle_collision_exciter.hpp:158
void excite(SampleType amount01) noexcept
Definition particle_collision_exciter.hpp:102
SampleType sustain_floor() const noexcept
Definition particle_collision_exciter.hpp:144
static constexpr std::size_t retained_bytes() noexcept
Definition particle_collision_exciter.hpp:154
void set_energy_decay(SampleType milliseconds) noexcept
Definition particle_collision_exciter.hpp:73
void set_collision_energy_loss(SampleType loss) noexcept
Absolute normalized-energy decrement applied after each collision.
Definition particle_collision_exciter.hpp:90
int particle_count() const noexcept
Definition particle_collision_exciter.hpp:137
static constexpr int latency_samples() noexcept
Definition particle_collision_exciter.hpp:153
static constexpr int kMaxParticleCount
Definition particle_collision_exciter.hpp:28
static constexpr std::uint64_t kDefaultSeed
Definition particle_collision_exciter.hpp:31
void set_event_rate(SampleType events_per_second) noexcept
Definition particle_collision_exciter.hpp:67
Definition additive_bank.hpp:218
constexpr std::uint64_t mix64(std::uint64_t z)
splitmix64 finalizer (Steele, Lea & Flood, OOPSLA 2014).
Definition rng.hpp:140
ParticleCollisionExciterT< float > ParticleCollisionExciter
Definition particle_collision_exciter.hpp:208
@ u
Definition formant_filter_bank.hpp:20
ParticleCollisionExciterT< double > ParticleCollisionExciter64
Definition particle_collision_exciter.hpp:209
Deterministic randomness for modulation: a counter-free PRNG, a Gaussian draw, a stateless purpose-ke...
Definition particle_collision_exciter.hpp:33
SampleType amplitude
Definition particle_collision_exciter.hpp:35
bool occurred
Definition particle_collision_exciter.hpp:34