DSPark 1.6.1
Header-only audio DSP framework in pure C++20 — zero dependencies
Loading...
Searching...
No Matches
SmoothedValue.h
Go to the documentation of this file.
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
83 void prepare(double sampleRate, double rampTimeMs = 20.0) noexcept
84 {
85 sampleRate_ = std::max(1.0, sampleRate);
86 rampTimeMs_ = std::max(0.1, rampTimeMs);
87
88 // Exponential: 1-pole coefficient
89 const double tau = rampTimeMs_ / 1000.0;
90 expCoeff_ = std::exp(-1.0 / (sampleRate_ * tau));
91
92 // Linear: Rate of change per sample (Rate Limiter)
93 linearRate_ = 1000.0 / (rampTimeMs_ * sampleRate_);
94
95 // Chase: Sample-rate correction ratio relative to 44.1kHz base
96 const double srRatio = 44100.0 / sampleRate_;
97 chaseMultDecay_ = std::pow(0.9999, srRatio);
98 chaseAddDecay_ = 0.01 * srRatio;
99 }
100
102 void setSmoothingType(SmoothingType type) noexcept { type_ = type; }
103
105 [[nodiscard]] SmoothingType getSmoothingType() const noexcept { return type_; }
106
112 void setTargetValue(T newTarget) noexcept
113 {
114 if (newTarget != newTarget) return; // NaN guard
115 if (newTarget != target_)
116 {
117 target_ = newTarget;
118 if (type_ == SmoothingType::Chase)
119 chaseSpeed_ = 2500.0; // Reset chase velocity on target change
120 }
121 }
122
127 [[nodiscard]] T getNextValue() noexcept
128 {
129 const double target = static_cast<double>(target_);
130 if (current_ == target) return target_;
131
132 switch (type_)
133 {
135 {
136 current_ = target + expCoeff_ * (current_ - target);
137 break;
138 }
140 {
141 if (current_ < target)
142 current_ = std::min(current_ + linearRate_, target);
143 else
144 current_ = std::max(current_ - linearRate_, target);
145 break;
146 }
148 {
149 current_ = target;
150 break;
151 }
153 {
154 chaseSpeed_ = std::max(350.0, std::min(2500.0, chaseSpeed_ * chaseMultDecay_ - chaseAddDecay_));
155 current_ = (current_ * chaseSpeed_ + target) / (chaseSpeed_ + 1.0);
156 break;
157 }
158 }
159
160 // Exact arrival: snap once within a relative epsilon of the target.
161 // Relative because with large magnitudes (e.g. a frequency of 10000)
162 // a purely absolute threshold would be finer than the float ulp.
163 const double eps = kEpsilon * std::max(1.0, std::abs(target));
164 if (std::abs(current_ - target) < eps)
165 current_ = target;
166
167 return static_cast<T>(current_);
168 }
169
181 void processBlock(std::span<T> buffer) noexcept
182 {
183 const double target = static_cast<double>(target_);
184 const size_t n = buffer.size();
185 if (current_ == target)
186 {
187 std::fill(buffer.begin(), buffer.end(), target_);
188 return;
189 }
190
191 // Work on locals: the compiler cannot prove the span does not alias
192 // the members, so member accesses would be re-loaded each iteration.
193 // The per-sample arrival snap matches getNextValue(); once settled,
194 // the rest of the block is a plain fill.
195 double current = current_;
196 const double eps = kEpsilon * std::max(1.0, std::abs(target));
197 size_t i = 0;
198
199 switch (type_)
200 {
202 {
203 const double coeff = expCoeff_;
204 for (; i < n; ++i)
205 {
206 current = target + coeff * (current - target);
207 if (std::abs(current - target) < eps) { current = target; break; }
208 buffer[i] = static_cast<T>(current);
209 }
210 break;
211 }
212
214 {
215 const double rate = linearRate_;
216 for (; i < n; ++i)
217 {
218 if (current < target) current = std::min(current + rate, target);
219 else if (current > target) current = std::max(current - rate, target);
220 if (std::abs(current - target) < eps) { current = target; break; }
221 buffer[i] = static_cast<T>(current);
222 }
223 break;
224 }
225
227 current = target;
228 break;
229
231 {
232 double speed = chaseSpeed_;
233 const double multDecay = chaseMultDecay_;
234 const double addDecay = chaseAddDecay_;
235 for (; i < n; ++i)
236 {
237 speed = std::max(350.0, std::min(2500.0, speed * multDecay - addDecay));
238 current = (current * speed + target) / (speed + 1.0);
239 if (std::abs(current - target) < eps) { current = target; break; }
240 buffer[i] = static_cast<T>(current);
241 }
242 chaseSpeed_ = speed;
243 break;
244 }
245 }
246
247 // Settled (or Disabled): the remainder of the block is the target.
248 for (; i < n; ++i)
249 buffer[i] = target_;
250
251 current_ = current;
252 }
253
255 [[nodiscard]] T getCurrentValue() const noexcept { return static_cast<T>(current_); }
256
258 [[nodiscard]] T getTargetValue() const noexcept { return target_; }
259
261 [[nodiscard]] bool isSmoothing() const noexcept { return current_ != static_cast<double>(target_); }
262
264 void skip() noexcept { current_ = static_cast<double>(target_); chaseSpeed_ = 350.0; }
265
267 void reset(T value = T(0)) noexcept
268 {
269 target_ = value;
270 current_ = static_cast<double>(value);
271 chaseSpeed_ = 350.0;
272 }
273
275 void setRampTime(double sampleRate, double rampTimeMs) noexcept
276 {
277 prepare(sampleRate, rampTimeMs);
278 }
279
280private:
281 double current_{ 0.0 };
282 T target_{ T(0) };
284
285 // DSP coefficients (double: recursive state and its drivers, see @class)
286 double expCoeff_{ 0.0 };
287 double linearRate_{ 0.0 };
288
289 // Chase state & coefficients
290 double chaseSpeed_{ 350.0 };
291 double chaseMultDecay_{ 0.9999 };
292 double chaseAddDecay_{ 0.01 };
293
294 double sampleRate_{ 44100.0 };
295 double rampTimeMs_{ 20.0 };
296
297 static constexpr double kEpsilon = 1e-7;
298};
299
300} // namespace dspark
Core mathematical utilities for digital signal processing.
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 (Airwindows style). 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.