DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
Loading...
Searching...
No Matches
AnalogRandom.h
1// DSPark -- Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi -- MIT License
3
4#pragma once
5
47#include <algorithm>
48#include <array>
49#include <atomic>
50#include <chrono>
51#include <cmath>
52#include <cstdint>
53#include <span>
54#include <type_traits>
55
56#include "AnalogConstants.h"
57
62namespace dspark
63{
68 namespace AnalogRandom
69 {
70 //==============================================================================
71 // PRNG and Internal Details
72 // (Reference constants live in Core/AnalogConstants.h, included above.)
73 //==============================================================================
74 namespace Detail
75 {
81 {
82 std::uint64_t s[4];
83
84 explicit Xoshiro256pp(std::uint64_t seed = 1) noexcept
85 {
86 reseed(seed);
87 }
88
89 void reseed(std::uint64_t seed) noexcept
90 {
91 if (seed == 0) seed = 1;
92 s[0] = splitmix64(seed + 0x9E3779B97F4A7C15ull);
93 s[1] = splitmix64(s[0]);
94 s[2] = splitmix64(s[1]);
95 s[3] = splitmix64(s[2]);
96 }
97
98 [[nodiscard]] std::uint64_t next() noexcept
99 {
100 const std::uint64_t result = rotl(s[0] + s[3], 23) + s[0];
101 const std::uint64_t t = s[1] << 17;
102 s[2] ^= s[0];
103 s[3] ^= s[1];
104 s[1] ^= s[2];
105 s[0] ^= s[3];
106 s[2] ^= t;
107 s[3] = rotl(s[3], 45);
108 return result;
109 }
110
111 [[nodiscard]] double next_double() noexcept
112 {
113 const std::uint64_t x = next();
114 constexpr double inv2pow53 = 1.0 / 9007199254740992.0;
115 return static_cast<double>(x >> 11) * inv2pow53;
116 }
117
118 private:
119 static std::uint64_t rotl(std::uint64_t x, int k) noexcept
120 {
121 return (x << k) | (x >> (64 - k));
122 }
123
124 static std::uint64_t splitmix64(std::uint64_t x) noexcept
125 {
126 x += 0x9E3779B97F4A7C15ull;
127 x = (x ^ (x >> 30)) * 0xBF58476D1CE4E5B9ull;
128 x = (x ^ (x >> 27)) * 0x94D049BB133111EBull;
129 return x ^ (x >> 31);
130 }
131 };
132
133 // NOTE: lock-free safety is checked at runtime per Generator instance
134 // (checkLockFree() -> isSafeToRun_). A preprocessor #warning here would
135 // fire on every translation unit unconditionally (the directive ignores
136 // the runtime `if`), so it is intentionally omitted.
137 }
138
139 //==============================================================================
140 // Public API
141 //==============================================================================
142
147 enum class NoiseType
148 {
149 Pink,
150 Brown,
151 White
152 };
153
160
165
171 template <typename Real = float>
173 {
174 static_assert(std::is_floating_point_v<Real>,
175 "AnalogRandom::Generator requires a floating-point Real type.");
176
177 public:
178 Generator() noexcept : prng_(generateUniqueSeed())
179 {
180 checkLockFree();
181 }
182
183 explicit Generator(std::uint64_t seed) noexcept : prng_(seed)
184 {
185 checkLockFree();
186 }
187
188 // std::atomic members are not move-constructible by default, but we
189 // own this Generator's state and need it to be storable in standard
190 // containers (e.g. std::vector). The atomic loads/stores below are
191 // safe because the source object is being moved-from; by contract
192 // it is not concurrently observed by any other thread.
193 Generator(Generator&& other) noexcept
194 : isSafeToRun_(other.isSafeToRun_),
195 prng_(other.prng_),
196 sampleRate_(other.sampleRate_),
197 phaseAccumulator_(other.phaseAccumulator_),
198 triggerNext_(other.triggerNext_.load(std::memory_order_relaxed)),
199 currentValue_(other.currentValue_),
200 targetValue_(other.targetValue_),
201 noiseType_(other.noiseType_.load(std::memory_order_relaxed)),
202 useBpmSync_(other.useBpmSync_.load(std::memory_order_relaxed)),
203 rateHz_(other.rateHz_.load(std::memory_order_relaxed)),
204 bpm_(other.bpm_.load(std::memory_order_relaxed)),
205 bpmDivision_(other.bpmDivision_.load(std::memory_order_relaxed)),
206 min_(other.min_.load(std::memory_order_relaxed)),
207 max_(other.max_.load(std::memory_order_relaxed)),
208 smoothingEnabled_(other.smoothingEnabled_.load(std::memory_order_relaxed)),
209 smoothingCoeff_(other.smoothingCoeff_.load(std::memory_order_relaxed)),
210 smoothingTimeMs_(other.smoothingTimeMs_.load(std::memory_order_relaxed)),
211 quantizationStep_(other.quantizationStep_.load(std::memory_order_relaxed)),
212 pendingSeed_(other.pendingSeed_.load(std::memory_order_relaxed)),
213 publishedValue_(other.publishedValue_.load(std::memory_order_relaxed)),
214 publishedPhase_(other.publishedPhase_.load(std::memory_order_relaxed)),
215 brownNoiseState_(other.brownNoiseState_),
216 pinkNoiseOctaves_(other.pinkNoiseOctaves_),
217 denormalFlip_(other.denormalFlip_)
218 {
219 }
220
221 Generator& operator=(Generator&& other) noexcept
222 {
223 if (this == &other) return *this;
224 isSafeToRun_ = other.isSafeToRun_;
225 prng_ = other.prng_;
226 sampleRate_ = other.sampleRate_;
227 phaseAccumulator_ = other.phaseAccumulator_;
228 triggerNext_.store(other.triggerNext_.load(std::memory_order_relaxed), std::memory_order_relaxed);
229 currentValue_ = other.currentValue_;
230 targetValue_ = other.targetValue_;
231 noiseType_.store (other.noiseType_.load(std::memory_order_relaxed), std::memory_order_relaxed);
232 useBpmSync_.store (other.useBpmSync_.load(std::memory_order_relaxed), std::memory_order_relaxed);
233 rateHz_.store (other.rateHz_.load(std::memory_order_relaxed), std::memory_order_relaxed);
234 bpm_.store (other.bpm_.load(std::memory_order_relaxed), std::memory_order_relaxed);
235 bpmDivision_.store(other.bpmDivision_.load(std::memory_order_relaxed), std::memory_order_relaxed);
236 min_.store (other.min_.load(std::memory_order_relaxed), std::memory_order_relaxed);
237 max_.store (other.max_.load(std::memory_order_relaxed), std::memory_order_relaxed);
238 smoothingEnabled_.store(other.smoothingEnabled_.load(std::memory_order_relaxed), std::memory_order_relaxed);
239 smoothingCoeff_.store (other.smoothingCoeff_.load(std::memory_order_relaxed), std::memory_order_relaxed);
240 smoothingTimeMs_.store (other.smoothingTimeMs_.load(std::memory_order_relaxed), std::memory_order_relaxed);
241 quantizationStep_.store(other.quantizationStep_.load(std::memory_order_relaxed), std::memory_order_relaxed);
242 pendingSeed_.store(other.pendingSeed_.load(std::memory_order_relaxed), std::memory_order_relaxed);
243 publishedValue_.store(other.publishedValue_.load(std::memory_order_relaxed), std::memory_order_relaxed);
244 publishedPhase_.store(other.publishedPhase_.load(std::memory_order_relaxed), std::memory_order_relaxed);
245 brownNoiseState_ = other.brownNoiseState_;
246 pinkNoiseOctaves_ = other.pinkNoiseOctaves_;
247 denormalFlip_ = other.denormalFlip_;
248 return *this;
249 }
250
251 // Copy operations stay deleted: the atomics make a meaningful copy
252 // semantically ambiguous (concurrent observers of the source) and
253 // we don't need them.
254 Generator(const Generator&) = delete;
255 Generator& operator=(const Generator&) = delete;
256
261 void prepare(double sampleRate) noexcept
262 {
263 if (sampleRate > 0.0 && std::isfinite(sampleRate)) sampleRate_ = sampleRate;
264 // The smoothing time is a time: re-derive its per-sample
265 // coefficient for this rate (setSmoothing() is usually called
266 // before prepare(), e.g. through setAnalogDefault()).
267 updateSmoothingCoeff();
268 reset();
269 }
270
274 void reset() noexcept
275 {
276 phaseAccumulator_ = 0.0;
277 triggerNext_.store(true, std::memory_order_relaxed);
278 currentValue_ = static_cast<Real>(0);
279 targetValue_ = static_cast<Real>(0);
280 brownNoiseState_ = static_cast<Real>(0);
281 pinkNoiseOctaves_.fill(static_cast<Real>(0));
282 publishedValue_.store(static_cast<Real>(0), std::memory_order_relaxed);
283 publishedPhase_.store(static_cast<Real>(0), std::memory_order_relaxed);
284 }
285
290 void reseed(std::uint64_t newSeed) noexcept
291 {
292 if (newSeed == 0) newSeed = 1;
293 pendingSeed_.store(newSeed, std::memory_order_release);
294 }
295
296 //----------------------------------------------------------------------
297 // Main audio API
298 //----------------------------------------------------------------------
299
305 [[nodiscard]] Real getNextSample() noexcept
306 {
307 if (!isSafeToRun_) [[unlikely]] return static_cast<Real>(0);
308
309 const auto pending = pendingSeed_.exchange(0, std::memory_order_acq_rel);
310 if (pending != 0)
311 {
312 prng_.reseed(pending);
313 reset();
314 }
315
316 updatePhase();
317
318 if (triggerNext_.exchange(false, std::memory_order_acquire))
319 {
320 generateNewTarget(tickColoredNoise(noiseType_.load(std::memory_order_relaxed)));
321 }
322
323 if (smoothingEnabled_.load(std::memory_order_relaxed))
324 {
325 const Real coeff = smoothingCoeff_.load(std::memory_order_relaxed);
326 currentValue_ += coeff * (targetValue_ - currentValue_);
327 }
328 else
329 {
330 currentValue_ = targetValue_;
331 }
332
333 // Professional DC-free denormal mitigation
334 denormalFlip_ = -denormalFlip_;
335 currentValue_ += denormalFlip_;
336
337 // Publish the readout words for getCurrentValue()/getPhase()
338 // (relaxed atomic stores: a plain MOV on x86/ARM64; the
339 // cross-thread read of the previous plain members was a data
340 // race -- undefined behaviour, not merely a stale read).
341 publishedValue_.store(currentValue_, std::memory_order_relaxed);
342 publishedPhase_.store(static_cast<Real>(phaseAccumulator_),
343 std::memory_order_relaxed);
344
345 return currentValue_;
346 }
347
353 void getNextBlock(std::span<Real> outputBuffer) noexcept
354 {
355 if (!isSafeToRun_ || outputBuffer.empty()) [[unlikely]] return;
356
357 const auto pending = pendingSeed_.exchange(0, std::memory_order_acq_rel);
358 if (pending != 0)
359 {
360 prng_.reseed(pending);
361 reset();
362 }
363
364 // Cache atomics to local registers for the block duration
365 const bool smoothing = smoothingEnabled_.load(std::memory_order_relaxed);
366 const Real coeff = smoothingCoeff_.load(std::memory_order_relaxed);
367 const NoiseType noiseType = noiseType_.load(std::memory_order_relaxed);
368 const Real quantStep = quantizationStep_.load(std::memory_order_relaxed);
369 Real currentMin = min_.load(std::memory_order_relaxed);
370 Real currentMax = max_.load(std::memory_order_relaxed);
371
372 if (currentMin > currentMax) std::swap(currentMin, currentMax);
373
374 for (Real& sample : outputBuffer)
375 {
376 updatePhase();
377
378 if (triggerNext_.exchange(false, std::memory_order_acquire))
379 {
380 // Clamp to [-1,1] before mapping, matching generateNewTarget()
381 // (pink noise can momentarily exceed unity).
382 const Real cn = std::clamp(tickColoredNoise(noiseType),
383 static_cast<Real>(-1), static_cast<Real>(1));
384 targetValue_ = currentMin + ((cn * static_cast<Real>(0.5)) + static_cast<Real>(0.5)) * (currentMax - currentMin);
385 if (quantStep > static_cast<Real>(0)) targetValue_ = std::round(targetValue_ / quantStep) * quantStep;
386 }
387
388 if (smoothing)
389 {
390 currentValue_ += coeff * (targetValue_ - currentValue_);
391 }
392 else
393 {
394 currentValue_ = targetValue_;
395 }
396
397 denormalFlip_ = -denormalFlip_;
398 currentValue_ += denormalFlip_;
399
400 sample = currentValue_;
401 }
402
403 // Publish the readout words once per block (block-boundary
404 // granularity is the documented readout resolution here; the
405 // per-sample loop above stays free of atomic stores).
406 publishedValue_.store(currentValue_, std::memory_order_relaxed);
407 publishedPhase_.store(static_cast<Real>(phaseAccumulator_),
408 std::memory_order_relaxed);
409 }
410
414 [[nodiscard]] Real getCurrentValue() const noexcept
415 {
416 return publishedValue_.load(std::memory_order_relaxed);
417 }
418
423 [[nodiscard]] Real getPhase() const noexcept
424 {
425 return publishedPhase_.load(std::memory_order_relaxed);
426 }
427
428 //----------------------------------------------------------------------
429 // Configuration API
430 //----------------------------------------------------------------------
431
432 void setNoiseType(NoiseType type) noexcept
433 {
434 noiseType_.store(type, std::memory_order_relaxed);
435 }
436
437 void setRateHz(Real rateInHz) noexcept
438 {
439 useBpmSync_.store(false, std::memory_order_relaxed);
440 rateHz_.store(static_cast<float>(rateInHz), std::memory_order_relaxed);
441 }
442
443 void setRateBPM(double bpm, BpmDivision division) noexcept
444 {
445 bpm_.store(static_cast<float>(bpm), std::memory_order_relaxed);
446 bpmDivision_.store(division, std::memory_order_relaxed);
447 useBpmSync_.store(true, std::memory_order_relaxed);
448 }
449
450 void updateBPM(double newBpm) noexcept
451 {
452 bpm_.store(static_cast<float>(newBpm), std::memory_order_relaxed);
453 }
454
458 template <typename T>
459 void setRange(T min, T max) noexcept
460 {
461 static_assert(std::is_floating_point_v<T>, "setRange only accepts floating-point types.");
462 if (min > max) std::swap(min, max);
463 min_.store(static_cast<Real>(min), std::memory_order_relaxed);
464 max_.store(static_cast<Real>(max), std::memory_order_relaxed);
465 }
466
474 void setSmoothing(bool shouldBeEnabled, Real timeInMs = static_cast<Real>(50.0)) noexcept
475 {
476 smoothingEnabled_.store(shouldBeEnabled, std::memory_order_relaxed);
477 smoothingTimeMs_.store(timeInMs > static_cast<Real>(0) ? static_cast<double>(timeInMs) : 0.0,
478 std::memory_order_relaxed);
479 updateSmoothingCoeff();
480 }
481
482 void setQuantization(Real step) noexcept
483 {
484 if (std::isnan(step) || step < static_cast<Real>(0)) step = static_cast<Real>(0);
485 quantizationStep_.store(step, std::memory_order_relaxed);
486 }
487
488 void setAnalogDefault(AnalogComponent component) noexcept
489 {
490 switch (component)
491 {
495 setRateHz(0.5f);
496 setSmoothing(true, 100.0f);
497 break;
501 setRateHz(2.0f);
502 setSmoothing(true, 50.0f);
503 break;
507 setRateHz(1.0f);
508 setSmoothing(true, 75.0f);
509 break;
513 setRateHz(1.5f);
514 setSmoothing(true, 80.0f);
515 break;
519 setRateHz(0.8f);
520 setSmoothing(true, 120.0f);
521 break;
522 }
523 }
524
525 template <typename Int>
526 [[nodiscard]] Int getNextDiscrete(Int imin, Int imax) noexcept
527 {
528 static_assert(std::is_integral_v<Int>, "getNextDiscrete requires an integral type.");
529 if (imax <= imin) return imin;
530 const Real value = getNextSample();
531
532 Real minVal = min_.load(std::memory_order_relaxed);
533 Real maxVal = max_.load(std::memory_order_relaxed);
534 if (minVal > maxVal) std::swap(minVal, maxVal);
535 if (maxVal == minVal) return imin;
536
537 const double t = static_cast<double>((value - minVal) / (maxVal - minVal));
538 const double mapped = t * static_cast<double>(imax - imin) + static_cast<double>(imin);
539 // llround/long long, not lround/long: on LLP64 platforms
540 // (Windows) long is 32-bit and would truncate 64-bit Int.
541 return static_cast<Int>(std::clamp(std::llround(mapped),
542 static_cast<long long>(imin),
543 static_cast<long long>(imax)));
544 }
545
546 [[nodiscard]] int getNextDiscreteInt(int imin, int imax) noexcept
547 {
548 return static_cast<int>(getNextDiscrete<int>(imin, imax));
549 }
550
551 private:
552 void checkLockFree() noexcept
553 {
554 // Guard against the atomics this class actually uses. On every
555 // supported platform these are all lock-free; a hypothetical
556 // platform where they are not degrades to silence instead of
557 // risking a blocking atomic on the audio thread.
558 isSafeToRun_ = std::atomic<Real>::is_always_lock_free
559 && std::atomic<float>::is_always_lock_free
560 && std::atomic<std::uint64_t>::is_always_lock_free
561 && std::atomic<double>::is_always_lock_free
562 && std::atomic<NoiseType>::is_always_lock_free
563 && std::atomic<BpmDivision>::is_always_lock_free
564 && std::atomic<bool>::is_always_lock_free;
565 }
566
568 void updateSmoothingCoeff() noexcept
569 {
570 const double timeMs = smoothingTimeMs_.load(std::memory_order_relaxed);
571 // A zero smoothing time means instantaneous. Keeping a previous
572 // coefficient here (possibly 0) would silently freeze the output
573 // short of every new target while smoothing is enabled.
574 const double coeff = (timeMs > 0.0)
575 ? 1.0 - std::exp(-1.0 / (sampleRate_ * timeMs / 1000.0))
576 : 1.0;
577 smoothingCoeff_.store(static_cast<Real>(coeff), std::memory_order_relaxed);
578 }
579
585 [[nodiscard]] Real tickColoredNoise(NoiseType type) noexcept
586 {
587 const Real white = static_cast<Real>(prng_.next_double() * 2.0 - 1.0);
588 switch (type)
589 {
590 case NoiseType::White: return white;
591 case NoiseType::Pink: return tickPinkNoise(white);
592 case NoiseType::Brown: return tickBrownNoise(white);
593 default: return white;
594 }
595 }
596
600 void generateNewTarget(Real sampledNoise) noexcept
601 {
602 sampledNoise = std::clamp(sampledNoise, static_cast<Real>(-1), static_cast<Real>(1));
603
604 Real currentMin = min_.load(std::memory_order_relaxed);
605 Real currentMax = max_.load(std::memory_order_relaxed);
606 if (currentMin > currentMax) std::swap(currentMin, currentMax);
607
608 targetValue_ = currentMin + ((sampledNoise * static_cast<Real>(0.5)) + static_cast<Real>(0.5)) * (currentMax - currentMin);
609
610 const Real quantStep = quantizationStep_.load(std::memory_order_relaxed);
611 if (quantStep > static_cast<Real>(0))
612 {
613 targetValue_ = std::round(targetValue_ / quantStep) * quantStep;
614 }
615 }
616
617 void updatePhase() noexcept
618 {
619 Real rate = static_cast<Real>(0);
620 if (useBpmSync_.load(std::memory_order_relaxed))
621 {
622 const float bpm = bpm_.load(std::memory_order_relaxed);
623 if (bpm > 0.0f)
624 {
625 const double noteLengthInBeats = 4.0 / getBpmDivisionMultiplier(bpmDivision_.load(std::memory_order_relaxed));
626 const double periodInSeconds = (60.0 / static_cast<double>(bpm)) * noteLengthInBeats;
627 if (periodInSeconds > 0.0) rate = static_cast<Real>(1.0 / periodInSeconds);
628 }
629 }
630 else
631 {
632 rate = static_cast<Real>(rateHz_.load(std::memory_order_relaxed));
633 }
634
635 if (rate <= static_cast<Real>(0)) return;
636
637 phaseAccumulator_ += static_cast<double>(rate) / sampleRate_;
638 if (phaseAccumulator_ >= 1.0)
639 {
640 // floor(), not a single -1.0: with rates at or above the
641 // sample rate the fixed decrement would let the
642 // accumulator grow without bound.
643 phaseAccumulator_ -= std::floor(phaseAccumulator_);
644 triggerNext_.store(true, std::memory_order_release);
645 }
646 }
647
648 [[nodiscard]] Real tickPinkNoise(Real white) noexcept
649 {
650 // Paul Kellett's refined 7-state pink-noise filter (public-domain
651 // "instrumentation-grade" variant): parallel one-pole bank whose
652 // summed response tracks -3 dB/oct across the audio band to
653 // within ~0.5 dB. The previous 3-pole truncation measured ~-5
654 // dB/oct, violating the documented pink slope.
655 Real b0 = pinkNoiseOctaves_[0];
656 Real b1 = pinkNoiseOctaves_[1];
657 Real b2 = pinkNoiseOctaves_[2];
658 Real b3 = pinkNoiseOctaves_[3];
659 Real b4 = pinkNoiseOctaves_[4];
660 Real b5 = pinkNoiseOctaves_[5];
661 Real b6 = pinkNoiseOctaves_[6];
662
663 b0 = static_cast<Real>(0.99886) * b0 + white * static_cast<Real>(0.0555179);
664 b1 = static_cast<Real>(0.99332) * b1 + white * static_cast<Real>(0.0750759);
665 b2 = static_cast<Real>(0.96900) * b2 + white * static_cast<Real>(0.1538520);
666 b3 = static_cast<Real>(0.86650) * b3 + white * static_cast<Real>(0.3104856);
667 b4 = static_cast<Real>(0.55000) * b4 + white * static_cast<Real>(0.5329522);
668 b5 = static_cast<Real>(-0.7616) * b5 - white * static_cast<Real>(0.0168980);
669
670 const Real pink = b0 + b1 + b2 + b3 + b4 + b5 + b6
671 + white * static_cast<Real>(0.5362);
672
673 pinkNoiseOctaves_[0] = b0;
674 pinkNoiseOctaves_[1] = b1;
675 pinkNoiseOctaves_[2] = b2;
676 pinkNoiseOctaves_[3] = b3;
677 pinkNoiseOctaves_[4] = b4;
678 pinkNoiseOctaves_[5] = b5;
679 pinkNoiseOctaves_[6] = white * static_cast<Real>(0.115926);
680
681 // Normalisation keeps the peak below unity for white in [-1,1].
682 // The peak grows with run length; measured worst case ~0.90 over
683 // 2^21 samples (RMS ~0.19), leaving headroom to full scale.
684 return pink * static_cast<Real>(0.11);
685 }
686
687 [[nodiscard]] Real tickBrownNoise(Real white) noexcept
688 {
689 brownNoiseState_ += white * static_cast<Real>(0.02);
690 brownNoiseState_ *= static_cast<Real>(0.995);
691 brownNoiseState_ = std::clamp(brownNoiseState_, static_cast<Real>(-1.0), static_cast<Real>(1.0));
692 return brownNoiseState_;
693 }
694
695 [[nodiscard]] static constexpr double getBpmDivisionMultiplier(BpmDivision division) noexcept
696 {
697 switch (division)
698 {
699 case BpmDivision::One: return 4.0;
700 case BpmDivision::Half: return 2.0;
701 case BpmDivision::Quarter: return 1.0;
702 case BpmDivision::Eighth: return 0.5;
703 case BpmDivision::Sixteenth: return 0.25;
704 case BpmDivision::ThirtySecond: return 0.125;
705 case BpmDivision::HalfTriplet: return 4.0 / 3.0;
706 case BpmDivision::QuarterTriplet: return 2.0 / 3.0;
707 case BpmDivision::EighthTriplet: return 1.0 / 3.0;
708 case BpmDivision::SixteenthTriplet: return 1.0 / 6.0;
709 case BpmDivision::DottedHalf: return 3.0;
710 case BpmDivision::DottedQuarter: return 1.5;
711 case BpmDivision::DottedEighth: return 0.75;
712 default: return 1.0;
713 }
714 }
715
716 [[nodiscard]] static std::uint64_t generateUniqueSeed() noexcept
717 {
718 static std::atomic_uint64_t instanceCounter{ 0 };
719 const auto instanceId = instanceCounter.fetch_add(1, std::memory_order_relaxed);
720 const auto thisPtrAddress = reinterpret_cast<std::uintptr_t>(&instanceCounter);
721 const auto now = static_cast<std::uint64_t>(std::chrono::steady_clock::now().time_since_epoch().count());
722 std::uint64_t seed = instanceId ^ (thisPtrAddress << 1) ^ (now + 0x9E3779B97F4A7C15ull);
723 return seed == 0 ? 1 : seed;
724 }
725
726 bool isSafeToRun_{ false };
727 Detail::Xoshiro256pp prng_;
728 double sampleRate_{ 44100.0 };
729 double phaseAccumulator_{ 0.0 };
730 std::atomic<bool> triggerNext_{ true };
731 Real currentValue_{ static_cast<Real>(0) };
732 Real targetValue_{ static_cast<Real>(0) };
733 std::atomic<NoiseType> noiseType_{ NoiseType::Pink };
734 std::atomic<bool> useBpmSync_{ false };
735 std::atomic<float> rateHz_{ 1.0f };
736 std::atomic<float> bpm_{ 120.0f };
737 std::atomic<BpmDivision> bpmDivision_{ BpmDivision::Quarter };
738 // Value-domain atomics use Real: with Real = double, storing them
739 // as float would silently round the user's range/quantization.
740 // Time-domain ones (rateHz_, bpm_) stay float on purpose.
741 std::atomic<Real> min_{ static_cast<Real>(-1) };
742 std::atomic<Real> max_{ static_cast<Real>(1) };
743 std::atomic<bool> smoothingEnabled_{ false };
744 std::atomic<Real> smoothingCoeff_{ static_cast<Real>(1) };
745 std::atomic<double> smoothingTimeMs_{ 0.0 };
746 std::atomic<Real> quantizationStep_{ static_cast<Real>(0) };
747 std::atomic<std::uint64_t> pendingSeed_{ 0 };
748 // Cross-thread READOUT words (each a single independent
749 // word): the generator thread publishes them with relaxed
750 // stores (once per getNextSample() call, and once per
751 // block in getNextBlock()); getCurrentValue()/getPhase() load
752 // them relaxed from any thread. The plain working members
753 // (currentValue_, phaseAccumulator_) stay audio-thread-private,
754 // so the hot recursion is untouched. No invariant couples the
755 // two words, so no seqlock is required.
756 std::atomic<Real> publishedValue_{ static_cast<Real>(0) };
757 std::atomic<Real> publishedPhase_{ static_cast<Real>(0) };
758 Real brownNoiseState_{ static_cast<Real>(0) };
759 std::array<Real, 7> pinkNoiseOctaves_{};
760 Real denormalFlip_{ static_cast<Real>(1e-18) }; // DC-Free denormal mitigation state
761 };
762 } // namespace AnalogRandom
763} // namespace dspark
Main generator class for analog-style random modulation.
void reset() noexcept
Reset internal state (phase, smoothing, noise states).
void setRateBPM(double bpm, BpmDivision division) noexcept
void setRateHz(Real rateInHz) noexcept
void setNoiseType(NoiseType type) noexcept
int getNextDiscreteInt(int imin, int imax) noexcept
void setSmoothing(bool shouldBeEnabled, Real timeInMs=static_cast< Real >(50.0)) noexcept
Enables one-pole smoothing of the held targets.
void reseed(std::uint64_t newSeed) noexcept
Request a lock-free reseed of the internal PRNG.
Int getNextDiscrete(Int imin, Int imax) noexcept
void prepare(double sampleRate) noexcept
Prepare the generator with the audio sample rate.
void setRange(T min, T max) noexcept
Set the output range for values. Safe from tearing.
Generator(const Generator &)=delete
Generator & operator=(const Generator &)=delete
Generator(Generator &&other) noexcept
void getNextBlock(std::span< Real > outputBuffer) noexcept
Generate a block of modulation samples.
Real getCurrentValue() const noexcept
Last published output value. Lock-free readout, any thread (relaxed atomic; updated once per getNextS...
Real getPhase() const noexcept
Last published LFO phase in [0, 1). Lock-free readout, any thread (relaxed atomic; same publication p...
void setQuantization(Real step) noexcept
void setAnalogDefault(AnalogComponent component) noexcept
Real getNextSample() noexcept
Generate and return the next modulation sample.
Generator(std::uint64_t seed) noexcept
Generator & operator=(Generator &&other) noexcept
void updateBPM(double newBpm) noexcept
Parametric multi-band EQ using cascaded biquads or FFT overlap-save convolution.
Definition Equalizer.h:72
Main namespace for the analog random generation library.
NoiseType
Defines the spectral distribution of the random fluctuations.
@ White
Uncorrelated noise for "sample-and-hold" effects.
@ Pink
1/f noise for organic drift.
@ Brown
1/f^2 noise for slow "wow/flutter".
Main namespace for the DSPark framework.
A compact xoshiro256++ PRNG implementation (non-threadsafe).
void reseed(std::uint64_t seed) noexcept
Xoshiro256pp(std::uint64_t seed=1) noexcept