DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
Loading...
Searching...
No Matches
SmoothedValue.h
1// DSPark -- Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi -- MIT License
3
4#pragma once
5
18#include "DspMath.h"
19
20#include <cmath>
21#include <algorithm>
22#include <span>
23
24namespace dspark {
25
59template <FloatType T>
61{
62public:
63 enum class SmoothingType
64 {
66 Linear,
67 Disabled,
68 Chase
69 };
70
72 SmoothedValue(const SmoothedValue&) = delete;
74
75 SmoothedValue() noexcept { prepare(sampleRate_, rampTimeMs_); }
76
85 void prepare(double sampleRate, double rampTimeMs = 20.0) noexcept
86 {
87 sampleRate_ = std::max(1.0, sampleRate);
88 // (a NaN ramp time also lands on 0: std::max returns its first argument.)
89 rampTimeMs_ = std::max(0.0, rampTimeMs);
90
91 // Exponential: 1-pole coefficient. A zero ramp used to be floored at
92 // 0.1 ms, so a caller asking for "no smoothing" (Gain documents
93 // non-positive ramps as 0) still got a few samples of glide.
94 const double tauSamples = sampleRate_ * rampTimeMs_ / 1000.0;
95 expCoeff_ = (tauSamples > 0.0) ? std::exp(-1.0 / tauSamples) : 0.0;
96
97 // Linear: ramp length in samples; the per-sample rate is sized from
98 // the remaining distance so an in-flight ramp keeps its new duration.
99 rampSamples_ = std::max(1.0, rampTimeMs_ * sampleRate_ / 1000.0);
100 updateLinearRate();
101
102 // Chase: Sample-rate correction ratio relative to 44.1kHz base
103 const double srRatio = 44100.0 / sampleRate_;
104 chaseMultDecay_ = std::pow(0.9999, srRatio);
105 chaseAddDecay_ = 0.01 * srRatio;
106 }
107
109 void setSmoothingType(SmoothingType type) noexcept
110 {
111 type_ = type;
112 updateLinearRate(); // a switch to Linear mid-flight keeps the full ramp time
113 }
114
116 [[nodiscard]] SmoothingType getSmoothingType() const noexcept { return type_; }
117
123 void setTargetValue(T newTarget) noexcept
124 {
125 if (newTarget != newTarget) return; // NaN guard
126 if (newTarget != target_)
127 {
128 target_ = newTarget;
129 updateLinearRate();
130 if (type_ == SmoothingType::Chase)
131 chaseSpeed_ = 2500.0; // Reset chase velocity on target change
132 }
133 }
134
139 [[nodiscard]] T getNextValue() noexcept
140 {
141 const double target = static_cast<double>(target_);
142 if (current_ == target) return target_;
143
144 switch (type_)
145 {
147 {
148 current_ = target + expCoeff_ * (current_ - target);
149 break;
150 }
152 {
153 if (current_ < target)
154 current_ = std::min(current_ + linearRate_, target);
155 else
156 current_ = std::max(current_ - linearRate_, target);
157 break;
158 }
160 {
161 current_ = target;
162 break;
163 }
165 {
166 chaseSpeed_ = std::max(350.0, std::min(2500.0, chaseSpeed_ * chaseMultDecay_ - chaseAddDecay_));
167 current_ = (current_ * chaseSpeed_ + target) / (chaseSpeed_ + 1.0);
168 break;
169 }
170 }
171
172 // Exact arrival: snap once within a relative epsilon of the target.
173 // Relative because with large magnitudes (e.g. a frequency of 10000)
174 // a purely absolute threshold would be finer than the float ulp.
175 const double eps = kEpsilon * std::max(1.0, std::abs(target));
176 if (std::abs(current_ - target) < eps)
177 current_ = target;
178
179 return static_cast<T>(current_);
180 }
181
193 void processBlock(std::span<T> buffer) noexcept
194 {
195 const double target = static_cast<double>(target_);
196 const size_t n = buffer.size();
197 if (current_ == target)
198 {
199 std::fill(buffer.begin(), buffer.end(), target_);
200 return;
201 }
202
203 // Work on locals: the compiler cannot prove the span does not alias
204 // the members, so member accesses would be re-loaded each iteration.
205 // The per-sample arrival snap matches getNextValue(); once settled,
206 // the rest of the block is a plain fill.
207 double current = current_;
208 const double eps = kEpsilon * std::max(1.0, std::abs(target));
209 size_t i = 0;
210
211 switch (type_)
212 {
214 {
215 const double coeff = expCoeff_;
216 for (; i < n; ++i)
217 {
218 current = target + coeff * (current - target);
219 if (std::abs(current - target) < eps) { current = target; break; }
220 buffer[i] = static_cast<T>(current);
221 }
222 break;
223 }
224
226 {
227 const double rate = linearRate_;
228 for (; i < n; ++i)
229 {
230 if (current < target) current = std::min(current + rate, target);
231 else if (current > target) current = std::max(current - rate, target);
232 if (std::abs(current - target) < eps) { current = target; break; }
233 buffer[i] = static_cast<T>(current);
234 }
235 break;
236 }
237
239 current = target;
240 break;
241
243 {
244 double speed = chaseSpeed_;
245 const double multDecay = chaseMultDecay_;
246 const double addDecay = chaseAddDecay_;
247 for (; i < n; ++i)
248 {
249 speed = std::max(350.0, std::min(2500.0, speed * multDecay - addDecay));
250 current = (current * speed + target) / (speed + 1.0);
251 if (std::abs(current - target) < eps) { current = target; break; }
252 buffer[i] = static_cast<T>(current);
253 }
254 chaseSpeed_ = speed;
255 break;
256 }
257 }
258
259 // Settled (or Disabled): the remainder of the block is the target.
260 for (; i < n; ++i)
261 buffer[i] = target_;
262
263 current_ = current;
264 }
265
267 [[nodiscard]] T getCurrentValue() const noexcept { return static_cast<T>(current_); }
268
270 [[nodiscard]] T getTargetValue() const noexcept { return target_; }
271
273 [[nodiscard]] bool isSmoothing() const noexcept { return current_ != static_cast<double>(target_); }
274
276 void skip() noexcept { current_ = static_cast<double>(target_); chaseSpeed_ = 350.0; }
277
279 void reset(T value = T(0)) noexcept
280 {
281 target_ = value;
282 current_ = static_cast<double>(value);
283 chaseSpeed_ = 350.0;
284 }
285
287 void setRampTime(double sampleRate, double rampTimeMs) noexcept
288 {
289 prepare(sampleRate, rampTimeMs);
290 }
291
292private:
293 void updateLinearRate() noexcept
294 {
295 linearRate_ = std::abs(static_cast<double>(target_) - current_) / rampSamples_;
296 }
297
298 double current_{ 0.0 };
299 T target_{ T(0) };
301
302 // DSP coefficients (double: recursive state and its drivers, see @class)
303 double expCoeff_{ 0.0 };
304 double linearRate_{ 0.0 };
305 double rampSamples_{ 882.0 };
306
307 // Chase state & coefficients
308 double chaseSpeed_{ 350.0 };
309 double chaseMultDecay_{ 0.9999 };
310 double chaseAddDecay_{ 0.01 };
311
312 double sampleRate_{ 44100.0 };
313 double rampTimeMs_{ 20.0 };
314
315 static constexpr double kEpsilon = 1e-7;
316};
317
318} // namespace dspark
Zero-allocation parameter smoother for real-time audio.
void setSmoothingType(SmoothingType type) noexcept
Sets the smoothing algorithm.
SmoothedValue & operator=(const SmoothedValue &)=delete
void reset(T value=T(0)) noexcept
Hard-resets both current and target states to a specific value.
void setRampTime(double sampleRate, double rampTimeMs) noexcept
Updates sample rate and/or ramp time, recalculating internal steps.
T getNextValue() noexcept
Calculates and returns the next smoothed value.
T getCurrentValue() const noexcept
Returns the current internal value without advancing the state.
void skip() noexcept
Forces the current value to instantly match the target, bypassing time.
bool isSmoothing() const noexcept
Evaluates if the smoother is actively transitioning.
void setTargetValue(T newTarget) noexcept
Updates the target value. Safe to call continuously (e.g., from host automation).
@ Linear
Rate-limited ramp. Constant velocity, exact target arrival.
@ Disabled
Instant snapping, no smoothing applied.
@ Exponential
One-pole IIR. Natural, musical interpolation.
@ Chase
Adaptive speed. Gentle start after a jump, accelerating settle.
void processBlock(std::span< T > buffer) noexcept
Computes a block of smoothed values into an output buffer.
SmoothedValue(const SmoothedValue &)=delete
Prevents accidental copying of stateful DSP objects.
SmoothingType getSmoothingType() const noexcept
Returns the current smoothing algorithm.
T getTargetValue() const noexcept
Returns the set target value.
void prepare(double sampleRate, double rampTimeMs=20.0) noexcept
Precalculates internal coefficients based on sample rate and timing.
Main namespace for the DSPark framework.