Sound Byte Libs 0.5.1-121-g3358a44
C++ firmware library for audio applications on 32-bit ARM Cortex-M processors
Loading...
Searching...
No Matches
analog_drift.hpp
Go to the documentation of this file.
1// sbl/dsp/comp/analog_drift.hpp — Analog oscillator drift simulation
2//
3// Slow stochastic pitch wander that simulates analog oscillator instability.
4// Brownian random walk with mean reversion, heavily smoothed to produce
5// sub-Hz movement in the ±cents range.
6//
7// Each instance has an independent PRNG — two instances produce uncorrelated
8// drift, which is critical for realistic beating between oscillators.
9//
10// Call process() at control rate (~200 Hz). Output is in semitones.
11// Multiply by the oscillator's frequency ratio to apply:
12// float freq = base_hz * semitones_to_ratio(drift.process(amount));
13//
14// Usage:
15// sbl::dsp::comp::AnalogDrift drift(42); // unique seed per instance
16// drift.set_rate(200.0f); // control rate in Hz
17// float offset = drift.process(0.05f); // ±5 cents peak, in semitones
18
19#ifndef SBL_DSP_COMP_ANALOG_DRIFT_HPP_
20#define SBL_DSP_COMP_ANALOG_DRIFT_HPP_
21
23
24namespace sbl::dsp::comp {
25
27public:
28 /// @note All public methods are ISR-safe — bounded computation, no I/O.
29
30 /// Construct with PRNG seed. Use a different seed for each instance.
31 explicit AnalogDrift(uint32_t seed = 1) : rng_(seed) {}
32
33 /// Set the control rate in Hz. Adjusts walk step and smoothing
34 /// to maintain consistent drift character regardless of call rate.
35 /// Call once at init, not per-sample.
36 void set_rate(float control_rate_hz) {
37 // Scale walk step inversely with rate so drift speed is rate-independent.
38 // At 200 Hz: step_scale = 0.01, at 1000 Hz: step_scale = 0.002
39 step_scale_ = 2.0f / control_rate_hz;
40
41 // Mean-revert decay per tick. Target time constant ~2 seconds.
42 // decay = exp(-1 / (tau * rate)) ≈ 1 - 1/(tau * rate) for small values
43 decay_ = 1.0f - 1.0f / (2.0f * control_rate_hz);
44
45 // LP smoothing coefficient. Target time constant ~0.5 seconds.
46 // Produces sub-Hz movement (the "slow wander" character).
47 smooth_ = 1.0f / (0.5f * control_rate_hz);
48 }
49
50 /// Process one control tick.
51 /// @param amount Peak deviation in semitones (0.05 = ±5 cents)
52 /// @return Pitch offset in semitones
53 float process(float amount) {
54 // Brownian walk: accumulate small random steps
55 walk_ += rng_.bipolar() * step_scale_;
56
57 // Mean reversion: prevent unbounded wander
58 walk_ *= decay_;
59
60 // Heavy LP smoothing: sub-Hz movement
61 filtered_ += smooth_ * (walk_ - filtered_);
62
63 return filtered_ * amount;
64 }
65
66 /// Current drift value (before amount scaling), for diagnostics.
67 float value() const { return filtered_; }
68
69 /// Reset drift state to zero.
70 void reset() {
71 walk_ = 0.0f;
72 filtered_ = 0.0f;
73 }
74
75 /// Re-seed the PRNG (for test reproducibility).
76 void seed(uint32_t s) { rng_.seed(s); }
77
78private:
79 primitives::Rng rng_;
80 float walk_ = 0.0f;
81 float filtered_ = 0.0f;
82
83 // Rate-dependent coefficients (defaults for 200 Hz control rate)
84 float step_scale_ = 0.01f;
85 float decay_ = 0.9975f;
86 float smooth_ = 0.01f;
87};
88
89} // namespace sbl::dsp::comp
90
91#endif // SBL_DSP_COMP_ANALOG_DRIFT_HPP_
AnalogDrift(uint32_t seed=1)
Construct with PRNG seed. Use a different seed for each instance.
void set_rate(float control_rate_hz)
void reset()
Reset drift state to zero.
float value() const
Current drift value (before amount scaling), for diagnostics.
float process(float amount)
void seed(uint32_t s)
Re-seed the PRNG (for test reproducibility).
float bipolar()
Random float in [-1.0, 1.0].
Definition rng.hpp:39
void seed(uint32_t s)
Set state directly (for test reproducibility).
Definition rng.hpp:50
Compositions: subcircuits of primitives.
Lightweight pseudorandom number generator.