DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
Loading...
Searching...
No Matches
Panner.h
1// DSPark - Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi - MIT License
3
4#pragma once
5
32#include "../Core/AudioBuffer.h"
33#include "../Core/AudioSpec.h"
34#include "../Core/Biquad.h"
35#include "../Core/DspMath.h"
36#include "../Core/Smoothers.h"
37#include "../Core/StateBlob.h"
38#include "Delay.h"
39
40#include <algorithm>
41#include <atomic>
42#include <cmath>
43#include <cstdint>
44#include <limits>
45#include <vector>
46
47namespace dspark {
48
49template <typename T = float>
50class Panner
51{
52public:
53 // Removed virtual destructor to maintain zero-cost abstraction (no vtable).
54 ~Panner() = default;
55
57 enum class Algorithm
58 {
60 Binaural,
61 MidPan,
62 SidePan,
63 Haas,
65 };
66
72 void prepare(const AudioSpec& spec)
73 {
74 if (!spec.isValid()) return;
76 // Start settled on the configured pan: from 0 a preset panned hard
77 // right faded in from the centre over the smoothing time.
78 panSmoother_.reset(sampleRate_, panTimeConstantMs(smoothingTime_.load(std::memory_order_relaxed)),
79 static_cast<float>(pan_.load(std::memory_order_relaxed)));
80
81 float maxMs = std::max(binauralMaxITD_.load(std::memory_order_relaxed),
82 haasMaxDelay_.load(std::memory_order_relaxed));
83
84 AudioSpec monoSpec { sampleRate_, spec.maxBlockSize, 1 };
85 // +1 ms of headroom over the configured maximum: the ITD/Haas paths
86 // add a ~4-sample common base delay on top of the maximum, and the
87 // capacity clamp must never shave it off.
88 delayL_.prepareMs(monoSpec, static_cast<double>(maxMs) + 1.0);
89 delayR_.prepareMs(monoSpec, static_cast<double>(maxMs) + 1.0);
92 // The ITD/Haas delays glide more slowly than the gains (time constant
93 // = the whole smoothing time): a delay that moves fast is a Doppler
94 // pitch bend, up to 30 ms of travel for Haas.
95 delayL_.setSmoothingTime(smoothingTime_.load(std::memory_order_relaxed));
96 delayR_.setSmoothingTime(smoothingTime_.load(std::memory_order_relaxed));
97
98 spectralPan_ = std::numeric_limits<float>::quiet_NaN(); // new rate: rebuild
100 }
101
109 void setAlgorithm(Algorithm algo) noexcept
110 {
111 algo = static_cast<Algorithm>(std::clamp(static_cast<int>(algo), 0,
112 static_cast<int>(Algorithm::Spectral)));
113 algorithm_.store(algo, std::memory_order_relaxed);
114 }
115
121 void setPan(T position) noexcept
122 {
123 if (!std::isfinite(position)) return;
124 // Publish only: the smoother is audio-thread state, so the target is
125 // applied at the top of the next processBlock() (writing it here from
126 // a control thread raced against getNextValue()).
127 pan_.store(std::clamp(position, T(-1), T(1)), std::memory_order_relaxed);
128 }
129
135 void processBlock(AudioBufferView<T> buffer) noexcept
136 {
137 if (buffer.getNumChannels() < 2) return;
138
139 // Front-door non-finite guard: the Spectral algorithm's Biquad
140 // shelves and the Binaural/Haas delay lines carry recursive state
141 // that a NaN/Inf input would poison permanently. Scrub non-finite input
142 // to 0 before any algorithm runs. No-op on finite input (metrics
143 // byte-identical).
144 {
145 const int gN = buffer.getNumSamples();
146 for (int ch = 0; ch < buffer.getNumChannels(); ++ch)
147 {
148 T* d = buffer.getChannel(ch);
149 for (int i = 0; i < gN; ++i)
150 if (!std::isfinite(d[i])) d[i] = T(0);
151 }
152 }
153
154 // Apply control-thread publications (audio-thread application point).
155 if (smoothingDirty_.exchange(false, std::memory_order_acquire))
157 panTimeConstantMs(smoothingTime_.load(std::memory_order_relaxed)),
159
160 float pTarget = static_cast<float>(pan_.load(std::memory_order_relaxed));
162
163 switch (algorithm_.load(std::memory_order_relaxed))
164 {
165 case Algorithm::EqualPower: applyEqualPower(buffer, pTarget); break;
166 case Algorithm::Binaural: applyCombinedBinaural(buffer, pTarget); break;
167 case Algorithm::MidPan: applyMidPan(buffer, pTarget); break;
168 case Algorithm::SidePan: applySidePan(buffer, pTarget); break;
169 case Algorithm::Haas: applyHaas(buffer, pTarget); break;
170 case Algorithm::Spectral: applySpectral(buffer, pTarget); break;
171 }
172 }
173
175 void reset() noexcept
176 {
177 delayL_.reset();
178 delayR_.reset();
182 }
183
184 // -- Configuration -------------------------------------------------------
185 // All thread-safe; non-finite values are ignored. The ITD / Haas maxima
186 // size the delay lines in prepare(): raising them beyond the prepared
187 // value takes full effect on the next prepare().
188
189 void setBinauralMaxITD(float ms) noexcept
190 {
191 if (!std::isfinite(ms)) return;
192 binauralMaxITD_.store(std::max(0.0f, ms), std::memory_order_relaxed);
193 }
194 void setHaasMaxDelay(float ms) noexcept
195 {
196 if (!std::isfinite(ms)) return;
197 haasMaxDelay_.store(std::max(0.0f, ms), std::memory_order_relaxed);
198 }
199 void setSpectralFrequency(float hz) noexcept
200 {
201 if (!std::isfinite(hz)) return;
202 spectralFreq_.store(std::clamp(hz, 20.0f, 20000.0f), std::memory_order_relaxed);
203 }
204 void setSpectralMaxGain(float dB) noexcept
205 {
206 if (!std::isfinite(dB)) return;
207 spectralMaxGain_.store(dB, std::memory_order_relaxed);
208 }
209
213 void setSmoothingTime(float ms) noexcept
214 {
215 if (!std::isfinite(ms)) return;
216 smoothingTime_.store(std::max(0.0f, ms), std::memory_order_relaxed);
217 delayL_.setSmoothingTime(ms); // Delay's own publish/apply handoff
218 delayR_.setSmoothingTime(ms);
219 smoothingDirty_.store(true, std::memory_order_release);
220 }
221
222
224 [[nodiscard]] std::vector<uint8_t> getState() const
225 {
226 StateWriter w(stateId("PANR"), 1);
227 w.write("pan", pan_.load(std::memory_order_relaxed));
228 w.write("algorithm", static_cast<int32_t>(algorithm_.load(std::memory_order_relaxed)));
229 w.write("smoothing", smoothingTime_.load(std::memory_order_relaxed));
230 w.write("binauralITD", binauralMaxITD_.load(std::memory_order_relaxed));
231 w.write("haasDelay", haasMaxDelay_.load(std::memory_order_relaxed));
232 w.write("spectralFreq", spectralFreq_.load(std::memory_order_relaxed));
233 w.write("spectralGain", spectralMaxGain_.load(std::memory_order_relaxed));
234 return w.blob();
235 }
236
238 bool setState(const uint8_t* data, size_t size)
239 {
240 StateReader r(data, size);
241 if (!r.isValid() || r.processorId() != stateId("PANR")) return false;
242 setPan(static_cast<T>(r.read("pan", 0.0f)));
243 setAlgorithm(static_cast<Algorithm>(r.read("algorithm", 0))); // clamped inside
244 setSmoothingTime(r.read("smoothing", 50.0f));
245 setBinauralMaxITD(r.read("binauralITD", 0.66f));
246 setHaasMaxDelay(r.read("haasDelay", 30.0f));
247 setSpectralFrequency(r.read("spectralFreq", 4000.0f));
248 setSpectralMaxGain(r.read("spectralGain", 6.0f));
249 return true;
250 }
251
252protected:
253 void applyEqualPower(AudioBufferView<T> buffer, float /*panTarget*/) noexcept
254 {
255 T* L = buffer.getChannel(0);
256 T* R = buffer.getChannel(1);
257 const int n = buffer.getNumSamples();
258 constexpr T halfPi = pi<T> / T(2);
259
261 {
262 // Static pan: hoist the trig out of the loop entirely.
263 const T angle = (static_cast<T>(panSmoother_.getCurrentValue()) * T(0.5) + T(0.5)) * halfPi;
264 const T gL = std::cos(angle);
265 const T gR = std::sin(angle);
266 for (int i = 0; i < n; ++i)
267 {
268 L[i] *= gL;
269 R[i] *= gR;
270 }
271 return;
272 }
273
274 for (int i = 0; i < n; ++i)
275 {
276 T p = static_cast<T>(panSmoother_.getNextValue());
277 T angle = (p * T(0.5) + T(0.5)) * halfPi;
278
279 // fastSin/fastCos: > 100 dB accurate, several times cheaper than libm.
280 L[i] *= fastCos(angle);
281 R[i] *= fastSin(angle);
282 }
283 }
284
285 void applyCombinedBinaural(AudioBufferView<T> buffer, float panTarget) noexcept
286 {
287 T* L = buffer.getChannel(0);
288 T* R = buffer.getChannel(1);
289 const int n = buffer.getNumSamples();
290
291 T itdMax = T(binauralMaxITD_.load(std::memory_order_relaxed));
292 T targetP = static_cast<T>(panTarget);
293
294 // Push the new ITD targets. A small COMMON base delay is added to both ears
295 // so neither hits the delay line's 3-sample interpolation floor: that floor
296 // used to clamp both ears to 3 for |pan| < ~0.1, killing the ITD cue near
297 // centre (a localization dead-zone). With the base offset the ITD difference
298 // is linear from the centre, and the shared delay is inaudible.
299 const T baseMs = T(4000) / static_cast<T>(sampleRate_); // ~4 samples
300 delayL_.setDelayMs(baseMs + itdMax * std::max(T(0), targetP)); // pan>0 => L delayed
301 delayR_.setDelayMs(baseMs + itdMax * std::max(T(0), -targetP)); // pan<0 => R delayed
302
303 for (int i = 0; i < n; ++i)
304 {
305 T p = static_cast<T>(panSmoother_.getNextValue());
306
307 // Binaural cross-feed model:
308 // - The ipsilateral ear (closer to the source) receives the
309 // near channel essentially intact + a small leakage from the
310 // far channel.
311 // - The contralateral ear (far ear) receives the far channel
312 // attenuated + a leakage from the near channel + the ITD
313 // delay applied later by delayL_/delayR_.
314 // This preserves audibility on both ears at hard pan, unlike a
315 // straight mute, and produces the head-shadowing illusion typical
316 // of real-world hearing (~6 dB ILD between ears at 90 deg).
317 T absp = std::abs(p); // 0..1
318 T farAtten = T(1) - absp * T(0.5); // 1.0 -> 0.5 at extreme
319 T leakage = absp * T(0.3); // 0 -> 0.3 cross-bleed
320
321 T l_temp, r_temp;
322 if (p >= T(0))
323 {
324 // Pan right -> L is the contralateral (delayed) ear.
325 l_temp = L[i] * farAtten + R[i] * leakage;
326 r_temp = R[i] + L[i] * leakage * T(0.5); // gentle near-ear bleed
327 }
328 else
329 {
330 // Pan left -> R is the contralateral ear.
331 l_temp = L[i] + R[i] * leakage * T(0.5);
332 r_temp = R[i] * farAtten + L[i] * leakage;
333 }
334
335 // Delay::processSample already advances its write index, so we
336 // must NOT call advanceWriteIndex() afterwards.
337 L[i] = delayL_.processSample(0, l_temp);
338 R[i] = delayR_.processSample(0, r_temp);
339 }
340 }
341
342 void applyMidPan(AudioBufferView<T> buffer, float /*panTarget*/) noexcept
343 {
344 T* L = buffer.getChannel(0);
345 T* R = buffer.getChannel(1);
346 const int n = buffer.getNumSamples();
347 constexpr T halfPi = pi<T> / T(2);
348
349 for (int i = 0; i < n; ++i)
350 {
351 T p = static_cast<T>(panSmoother_.getNextValue());
352 T mid = (L[i] + R[i]) * T(0.5);
353 T side = (L[i] - R[i]) * T(0.5);
354
355 // Equal-power mid pan: gL^2 + gR^2 == 2 for every position, with
356 // gL == gR == 1 at centre (transparent to within the fast-trig
357 // accuracy, > 100 dB). A hard pan peaks at
358 // +3 dB instead of the +6 dB of the old constant-voltage law,
359 // keeping headroom predictable while staying mono-compatible.
360 T angle = (p * T(0.5) + T(0.5)) * halfPi;
361 T gL = fastCos(angle) * sqrt2<T>;
362 T gR = fastSin(angle) * sqrt2<T>;
363
364 L[i] = mid * gL + side;
365 R[i] = mid * gR - side;
366 }
367 }
368
369 void applySidePan(AudioBufferView<T> buffer, float /*panTarget*/) noexcept
370 {
371 T* L = buffer.getChannel(0);
372 T* R = buffer.getChannel(1);
373 const int n = buffer.getNumSamples();
374 constexpr T halfPi = pi<T> / T(2);
375
376 for (int i = 0; i < n; ++i)
377 {
378 T p = static_cast<T>(panSmoother_.getNextValue());
379 T mid = (L[i] + R[i]) * T(0.5);
380 T side = (L[i] - R[i]) * T(0.5);
381
382 T angle = (p * T(0.5) + T(0.5)) * halfPi;
383
384 // Normalized by sqrt(2) to prevent -3dB attenuation at dead center.
385 // Clamped to avoid massive boosts at hard extremes.
386 T sideGainL = std::clamp(fastCos(angle) * sqrt2<T>, T(0), T(1));
387 T sideGainR = std::clamp(fastSin(angle) * sqrt2<T>, T(0), T(1));
388
389 L[i] = mid + (side * sideGainL);
390 R[i] = mid - (side * sideGainR);
391 }
392 }
393
394 void applyHaas(AudioBufferView<T> buffer, float panTarget) noexcept
395 {
396 T haasMax = T(haasMaxDelay_.load(std::memory_order_relaxed));
397 T pT = static_cast<T>(panTarget);
398
399 // Smoothed inside Delay - no abrupt pointer jumps even on fast moves.
400 // Common base delay keeps both ears above the 3-sample interpolation floor
401 // so the inter-channel delay is linear from centre (no dead-zone).
402 const T baseMs = T(4000) / static_cast<T>(sampleRate_); // ~4 samples
403 delayL_.setDelayMs(baseMs + haasMax * std::max(T(0), pT));
404 delayR_.setDelayMs(baseMs + haasMax * std::max(T(0), -pT));
405
406 T* L = buffer.getChannel(0);
407 T* R = buffer.getChannel(1);
408 const int n = buffer.getNumSamples();
409
410 for (int i = 0; i < n; ++i)
411 {
412 // Keep the pan smoother stepping in sync with the block, even
413 // though Haas does not use the smoothed pan value directly.
415
416 // processSample advances the write index by itself; calling
417 // advanceWriteIndex() afterwards corrupts the delay line.
418 L[i] = delayL_.processSample(0, L[i]);
419 R[i] = delayR_.processSample(0, R[i]);
420 }
421 }
422
423 void applySpectral(AudioBufferView<T> buffer, float /*panTarget*/) noexcept
424 {
425 // The shelves follow the SMOOTHED pan, re-designed every 4 samples
426 // while it moves (coarser steps left an audible coefficient zipper).
427 // Settled, the cached design is reused.
428 T* L = buffer.getChannel(0);
429 T* R = buffer.getChannel(1);
430 const int n = buffer.getNumSamples();
431 constexpr int kSubBlock = 4;
432
433 for (int start = 0; start < n; start += kSubBlock)
434 {
435 const int end = std::min(n, start + kSubBlock);
436 float p = panSmoother_.getCurrentValue();
437 for (int i = start; i < end; ++i)
439 updateSpectralFilters(static_cast<T>(p));
440
441 for (int i = start; i < end; ++i)
442 {
443 L[i] = spectralL_.processSample(L[i], 0);
444 R[i] = spectralR_.processSample(R[i], 0);
445 }
446 }
447 }
448
449 void updateSpectralFilters(T targetPan) noexcept
450 {
451 float sMaxGain = spectralMaxGain_.load(std::memory_order_relaxed);
452 float sFreq = spectralFreq_.load(std::memory_order_relaxed);
453 const float pan = static_cast<float>(targetPan);
454 if (pan == spectralPan_ && sFreq == spectralFreqUsed_ && sMaxGain == spectralGainUsed_)
455 return; // design unchanged
456 spectralPan_ = pan;
457 spectralFreqUsed_ = sFreq;
458 spectralGainUsed_ = sMaxGain;
459
460 T gainLdB = -targetPan * static_cast<T>(sMaxGain);
461 T gainRdB = targetPan * static_cast<T>(sMaxGain);
462
463 // Audio-thread-owned coefficients: the direct setter (setCoeffs() is
464 // the cross-thread channel; a stream owner must not self-publish).
466 sampleRate_, static_cast<double>(sFreq), static_cast<double>(gainLdB)));
468 sampleRate_, static_cast<double>(sFreq), static_cast<double>(gainRdB)));
469 }
470
471 double sampleRate_ = 48000.0;
472 std::atomic<Algorithm> algorithm_ { Algorithm::EqualPower };
473 std::atomic<T> pan_ { T(0) };
474
476 // Second-order (critically damped) pan smoothing: position AND velocity
477 // are continuous, so a control stream updated at GUI/automation rate
478 // (a new target every ~16 ms) moves without the 60 Hz kinks of a linear
479 // ramp restarted on every update (an audible zipper on fast drags).
483 static float panTimeConstantMs(float smoothingMs) noexcept { return smoothingMs * 0.2f; }
485
486 std::atomic<float> smoothingTime_ { 50.0f };
487 std::atomic<bool> smoothingDirty_ { false };
488 std::atomic<float> binauralMaxITD_ { 0.66f };
489 std::atomic<float> haasMaxDelay_ { 30.0f };
490 std::atomic<float> spectralFreq_ { 4000.0f };
491 std::atomic<float> spectralMaxGain_ { 6.0f };
492
493 // Settings of the current shelf design (audio thread; NaN forces a rebuild).
494 float spectralPan_ = std::numeric_limits<float>::quiet_NaN();
495 float spectralFreqUsed_ = 0.0f;
496 float spectralGainUsed_ = 0.0f;
497};
498
499} // namespace dspark
Non-owning view over audio channel data.
Definition AudioBuffer.h:50
Biquad filter using Transposed Direct Form II (TDF-II) with thread-safe updates.
Definition Biquad.h:661
void setCoeffsNow(const BiquadCoeffs &c) noexcept
Stream-owner direct set: makes c the active set immediately.
Definition Biquad.h:771
void reset() noexcept
Resets all per-channel filter states to zero to avoid ringing/clicks.
Definition Biquad.h:811
T processSample(T input, int channel) noexcept
Processes a single sample for a specific channel.
Definition Biquad.h:837
void setHaasMaxDelay(float ms) noexcept
Definition Panner.h:194
void setAlgorithm(Algorithm algo) noexcept
Sets the active panning algorithm safely from any thread.
Definition Panner.h:109
~Panner()=default
std::atomic< float > binauralMaxITD_
Definition Panner.h:488
void prepare(const AudioSpec &spec)
Initializes internal delays, filters, and smoothers.
Definition Panner.h:72
Delay< T > delayR_
Definition Panner.h:475
Biquad< T, 1 > spectralR_
Definition Panner.h:484
float spectralGainUsed_
Definition Panner.h:496
std::atomic< Algorithm > algorithm_
Definition Panner.h:472
void setSmoothingTime(float ms) noexcept
Sets the pan smoothing time: a change settles (to 96%) in this many ms along a critically damped curv...
Definition Panner.h:213
Delay< T > delayL_
Definition Panner.h:475
std::atomic< float > spectralMaxGain_
Definition Panner.h:491
std::vector< uint8_t > getState() const
Serializes the parameter state (setup/UI threads; allocates).
Definition Panner.h:224
Biquad< T, 1 > spectralL_
Definition Panner.h:484
void setPan(T position) noexcept
Sets the target pan position (automatable, smoothed).
Definition Panner.h:121
void setBinauralMaxITD(float ms) noexcept
Definition Panner.h:189
void applyEqualPower(AudioBufferView< T > buffer, float) noexcept
Definition Panner.h:253
std::atomic< float > haasMaxDelay_
Definition Panner.h:489
void setSpectralMaxGain(float dB) noexcept
Definition Panner.h:204
double sampleRate_
Definition Panner.h:471
static float panTimeConstantMs(float smoothingMs) noexcept
Definition Panner.h:483
Smoothers::CriticallyDampedSmoother panSmoother_
Definition Panner.h:480
void applySpectral(AudioBufferView< T > buffer, float) noexcept
Definition Panner.h:423
void reset() noexcept
Clears delay lines and filter states to prevent ghost echoes.
Definition Panner.h:175
void applyMidPan(AudioBufferView< T > buffer, float) noexcept
Definition Panner.h:342
std::atomic< T > pan_
Definition Panner.h:473
std::atomic< bool > smoothingDirty_
Pan smoother re-config pending.
Definition Panner.h:487
std::atomic< float > smoothingTime_
Definition Panner.h:486
void processBlock(AudioBufferView< T > buffer) noexcept
Processes an audio block in-place. Real-time safe.
Definition Panner.h:135
void applyHaas(AudioBufferView< T > buffer, float panTarget) noexcept
Definition Panner.h:394
void setSpectralFrequency(float hz) noexcept
Definition Panner.h:199
float spectralPan_
Definition Panner.h:494
void applyCombinedBinaural(AudioBufferView< T > buffer, float panTarget) noexcept
Definition Panner.h:285
void updateSpectralFilters(T targetPan) noexcept
Definition Panner.h:449
float spectralFreqUsed_
Definition Panner.h:495
std::atomic< float > spectralFreq_
Definition Panner.h:490
Algorithm
Available panning algorithms.
Definition Panner.h:58
@ Binaural
Cross-feeding + ITD delay.
@ EqualPower
Standard -3 dB constant-power pan.
@ MidPan
Pans only the centre (mid) image.
@ SidePan
Pans only the stereo (side) image.
@ Haas
Precedence effect via inter-channel delay.
@ Spectral
Frequency-dependent panning via high-shelf.
bool setState(const uint8_t *data, size_t size)
Restores parameters from a blob (tolerant; rejects foreign ids).
Definition Panner.h:238
void applySidePan(AudioBufferView< T > buffer, float) noexcept
Definition Panner.h:369
Tolerant reader: missing keys yield defaults, unknown keys are skipped.
Definition StateBlob.h:161
float read(const char *key, float defaultValue) const
Reads a float, or defaultValue when the key is absent.
Definition StateBlob.h:204
bool isValid() const noexcept
Definition StateBlob.h:199
uint32_t processorId() const noexcept
Definition StateBlob.h:200
Serializes key/value parameters into a versioned blob.
Definition StateBlob.h:53
std::vector< uint8_t > blob() const
Finalizes and returns the blob.
Definition StateBlob.h:105
void write(const char *key, float value)
Writes a float parameter.
Definition StateBlob.h:71
Main namespace for the DSPark framework.
T fastSin(T x) noexcept
Fast sine approximation (degree-9 odd minimax polynomial).
Definition DspMath.h:248
T fastCos(T x) noexcept
Fast cosine approximation. See fastSin() for accuracy notes (the pi/2 offset costs float about half a...
Definition DspMath.h:276
constexpr uint32_t stateId(const char(&tag)[5]) noexcept
Builds a FOURCC processor id, e.g. dspark::stateId("COMP").
Definition StateBlob.h:651
constexpr T halfPi
Pi / 2 (1.57079...). Quarter period; sin/cos phase offset.
Definition DspMath.h:54
Describes the audio environment for a DSP processor.
Definition AudioSpec.h:37
constexpr bool isValid() const noexcept
Checks if the specification contains valid, processable parameters.
Definition AudioSpec.h:71
int maxBlockSize
Maximum number of samples per processing block.
Definition AudioSpec.h:53
double sampleRate
Sample rate in Hz.
Definition AudioSpec.h:45
static BiquadCoeffs makeHighShelf(double sampleRate, double freq, double gainDb, double slope=1.0) noexcept
High-shelf filter.
Definition Biquad.h:344
Critically damped smoother (no overshoot, exact Q=0.5).
Definition Smoothers.h:314
void reset(double sampleRate, float timeConstantMilliseconds, float initialValue=0.0f) noexcept
Definition Smoothers.h:778
float getCurrentValue() const noexcept
Definition Smoothers.h:253
void setTargetValue(float newTarget) noexcept
Definition Smoothers.h:652