Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
noise_source.hpp
Go to the documentation of this file.
1#pragma once
2
4
5#include <algorithm>
6#include <array>
7#include <cmath>
8#include <cstddef>
9#include <cstdint>
10
11namespace pulp::signal {
12
24
51template <typename SampleType = float> class NoiseSourceT {
52 public:
55 static constexpr std::uint32_t default_seed = 0x1D872B41u;
56
61 prepare(sample_rate_);
62 }
63
66 void prepare(double sample_rate) {
67 sample_rate_ = sample_rate > 0.0 ? sample_rate : sample_rate_;
68 update_coefficients();
69 const NoiseColor selected = color_;
70 for (std::size_t index = 0; index < normalisations_.size(); ++index) {
71 color_ = static_cast<NoiseColor>(index);
72 measure_normalisation();
73 normalisations_[index] = norm_;
74 }
75 color_ = selected;
76 norm_ = normalisations_[static_cast<std::size_t>(color_)];
77 reset();
78 }
79
84 const auto index = std::min(static_cast<std::size_t>(c), normalisations_.size() - 1);
85 color_ = static_cast<NoiseColor>(index);
86 norm_ = normalisations_[index];
87 reset_filters();
88 }
89
90 NoiseColor color() const {
91 return color_;
92 }
93
96 void set_seed(std::uint32_t seed) {
97 seed_ = seed == 0 ? default_seed : seed;
98 }
99
100 std::uint32_t seed() const {
101 return seed_;
102 }
103
105 void reset() {
106 state_ = seed_;
107 reset_filters();
108 }
109
111 SampleType process() {
112 return static_cast<SampleType>(norm_ * colorise(white_next()));
113 }
114
115 void process(SampleType* buffer, int num_samples) {
116 for (int i = 0; i < num_samples; ++i)
117 buffer[i] = process();
118 }
119
123 SampleType white() {
124 return static_cast<SampleType>(white_next());
125 }
126
127 private:
128 // These poles are deliberately open-coded rather than built from
129 // TptFilterT, which is the repository's shared one-pole. They are not
130 // standalone filters: their weights are derived from the same
131 // impulse-invariant coefficient the recurrence uses, and the whole bank is
132 // normalised as one. TptFilterT pre-warps its corner through a tangent,
133 // which would move the top pole enough to tilt the slope the bank exists
134 // to produce. Anywhere a one-pole stands alone, use TptFilterT.
135 //
136 // Pink is approximated by summing one-pole lowpasses whose corners are
137 // spaced by a constant ratio, each weighted by 1/sqrt(fc). Between two
138 // adjacent corners the sum falls by exactly the corner ratio over exactly
139 // that span of frequency, which is a 1/f power spectrum; the residual
140 // ripple shrinks as the corners are packed closer. Five corners a factor of
141 // eight apart cover roughly 8 Hz to 30 kHz with the ripple small enough to
142 // stay inside a half-dB-per-octave slope tolerance.
143 static constexpr std::size_t kPinkPoles = 5;
144 static constexpr double kPinkBaseHz = 8.0;
145 static constexpr double kPinkSpacing = 8.0;
146
147 // Brown is a single one-pole well below the audio band, so its -6 dB/octave
148 // region starts under the lowest frequency anyone measures.
149 static constexpr double kBrownHz = 12.0;
150
151 // Target RMS for every colour, matching a uniform white source in [-1, 1).
152 static constexpr double kTargetRms = 0.57735026918962576; // 1/sqrt(3)
153
154 double white_next() {
155 state_ ^= state_ << 13;
156 state_ ^= state_ >> 17;
157 state_ ^= state_ << 5;
158 // [0, 2^32) -> [-1, 1). The divisor is 2^31 so the sign is symmetric.
159 return static_cast<double>(state_) * (1.0 / 2147483648.0) - 1.0;
160 }
161
162 double colorise(double w) {
163 switch (color_) {
165 return w;
166 case NoiseColor::pink:
167 return pink(w);
169 return brown(w);
170 case NoiseColor::blue:
171 return differentiate(pink(w));
173 return differentiate(w);
174 }
175 return w;
176 }
177
178 double pink(double w) {
179 double sum = 0.0;
180 for (std::size_t k = 0; k < kPinkPoles; ++k) {
181 pink_state_[k] = snap_to_zero(pink_state_[k] + pink_a_[k] * (w - pink_state_[k]));
182 sum += pink_w_[k] * pink_state_[k];
183 }
184 return sum;
185 }
186
187 double brown(double w) {
188 brown_state_ = snap_to_zero(brown_state_ + brown_a_ * (w - brown_state_));
189 return brown_state_;
190 }
191
192 double differentiate(double x) {
193 const double y = x - diff_state_;
194 diff_state_ = x;
195 return y;
196 }
197
198 void reset_filters() {
199 pink_state_.fill(0.0);
200 brown_state_ = 0.0;
201 diff_state_ = 0.0;
202 }
203
204 void update_coefficients() {
205 double weight_sum = 0.0;
206 for (std::size_t k = 0; k < kPinkPoles; ++k) {
207 const double fc = kPinkBaseHz * std::pow(kPinkSpacing, static_cast<double>(k));
208 // Keep every corner inside the band the sample rate can represent;
209 // a corner at or above Nyquist would alias its own coefficient.
210 const double clamped = std::min(fc, 0.45 * sample_rate_);
211 pink_a_[k] = 1.0 - std::exp(-2.0 * 3.14159265358979323846 * clamped / sample_rate_);
212 pink_w_[k] = 1.0 / std::sqrt(clamped);
213 weight_sum += pink_w_[k];
214 }
215 // Scale the weights to O(1) so the measured normalisation constant does
216 // not have to absorb an arbitrary magnitude.
217 for (std::size_t k = 0; k < kPinkPoles; ++k)
218 pink_w_[k] /= weight_sum;
219
220 brown_a_ = 1.0 - std::exp(-2.0 * 3.14159265358979323846 * kBrownHz / sample_rate_);
221 }
222
223 // Renders a fixed-length burst through a scratch copy of the current colour
224 // and solves for the gain that lands its RMS on `kTargetRms`. Measuring
225 // beats hand-tuning a table of per-colour constants: the same code stays
226 // correct when the sample rate, the pole layout, or the colour set changes.
227 void measure_normalisation() {
228 // White needs no measurement -- the generator's own range is the
229 // target -- and it is the default, so the common path costs nothing.
230 if (color_ == NoiseColor::white) {
231 norm_ = 1.0;
232 return;
233 }
234
235 constexpr int kBurst = 16384;
236 // Discard a lead-in so the slowest one-pole is past its step response
237 // before any sample is counted.
238 constexpr int kWarmup = 4096;
239
240 const std::uint32_t saved_state = state_;
241 const auto saved_pink = pink_state_;
242 const double saved_brown = brown_state_;
243 const double saved_diff = diff_state_;
244
245 state_ = seed_;
246 reset_filters();
247 for (int i = 0; i < kWarmup; ++i)
248 colorise(white_next());
249
250 double sum_sq = 0.0;
251 for (int i = 0; i < kBurst; ++i) {
252 const double y = colorise(white_next());
253 sum_sq += y * y;
254 }
255 const double rms = std::sqrt(sum_sq / kBurst);
256 norm_ = rms > 1e-12 ? kTargetRms / rms : 1.0;
257
258 state_ = saved_state;
259 pink_state_ = saved_pink;
260 brown_state_ = saved_brown;
261 diff_state_ = saved_diff;
262 }
263
264 double sample_rate_ = 44100.0;
266 std::uint32_t seed_ = default_seed;
267 std::uint32_t state_ = default_seed;
268 double norm_ = 1.0;
269 std::array<double, 5> normalisations_{1.0, 1.0, 1.0, 1.0, 1.0};
270
271 std::array<double, kPinkPoles> pink_a_{};
272 std::array<double, kPinkPoles> pink_w_{};
273 std::array<double, kPinkPoles> pink_state_{};
274 double brown_a_ = 0.0;
275 double brown_state_ = 0.0;
276 double diff_state_ = 0.0;
277};
278
281
282} // namespace pulp::signal
A deterministic, colourable noise generator.
Definition noise_source.hpp:51
std::uint32_t seed() const
Definition noise_source.hpp:100
void set_color(NoiseColor c)
Selects the spectral tilt.
Definition noise_source.hpp:83
void process(SampleType *buffer, int num_samples)
Definition noise_source.hpp:115
SampleType process()
Next sample, normalised so every colour has approximately the same RMS.
Definition noise_source.hpp:111
void reset()
Rewinds the generator to its seed and clears the colour filter state.
Definition noise_source.hpp:105
void prepare(double sample_rate)
Fixes the sample rate and re-measures the per-colour normalisation.
Definition noise_source.hpp:66
NoiseSourceT()
A default-constructed source is immediately usable at 44.1 kHz, so a voice that forgets to call prepa...
Definition noise_source.hpp:60
void set_seed(std::uint32_t seed)
Sets the seed used by reset().
Definition noise_source.hpp:96
SampleType white()
Raw uniform white sample in [-1, 1), bypassing the colour filter and the normalisation.
Definition noise_source.hpp:123
static constexpr std::uint32_t default_seed
Definition noise_source.hpp:55
NoiseColor color() const
Definition noise_source.hpp:90
Definition additive_bank.hpp:218
@ buffer
A borrowed static buffer with a traversable playhead.
Definition granular.hpp:110
@ rms
Running mean-square. Reads program loudness.
Definition feedforward_compressor.hpp:120
NoiseSourceT< double > NoiseSource64
Definition noise_source.hpp:280
@ c
Definition spectrum_trace.hpp:35
T snap_to_zero(T x)
Returns 0 if |x| < snap_threshold, else x.
Definition denormal.hpp:45
@ i
Definition formant_filter_bank.hpp:20
@ e
Definition formant_filter_bank.hpp:20
NoiseColor
Spectral tilt of a NoiseSourceT, named by the usual colour convention.
Definition noise_source.hpp:17
@ blue
+3 dB/octave — the mirror of pink.
Definition noise_source.hpp:21
@ pink
-3 dB/octave — equal power per octave.
Definition noise_source.hpp:19
@ brown
-6 dB/octave — integrated white ("red" noise).
Definition noise_source.hpp:20
@ violet
+6 dB/octave — differentiated white ("purple" noise).
Definition noise_source.hpp:22
@ white
0 dB/octave — the raw generator output.
Definition noise_source.hpp:18
NoiseSourceT< float > NoiseSource
Definition noise_source.hpp:279
float sum(const float *data, std::size_t count) noexcept
@ y
Definition input_events.hpp:42
@ k
Definition input_events.hpp:40
@ x
Definition input_events.hpp:41