DSPark 1.6.1
Header-only audio DSP framework in pure C++20 — zero dependencies
Loading...
Searching...
No Matches
Limiter.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
45#include "../Core/DspMath.h"
46#include "../Core/AudioSpec.h"
47#include "../Core/AudioBuffer.h"
48#include "../Core/RingBuffer.h"
49#include "../Core/SmoothedValue.h"
50#include "../Core/DenormalGuard.h"
51#include "../Core/TruePeakDetector.h"
52#include "../Core/StateBlob.h"
53
54#include <algorithm>
55#include <atomic>
56#include <cmath>
57#include <cstddef>
58#include <cstdint>
59#include <vector>
60
61namespace dspark {
62
69template <FloatType T>
71{
72public:
73 ~Limiter() = default; // non-virtual: leaf class (no virtual dispatch)
74
75 // -- Lifecycle --------------------------------------------------------------
76
93 void prepare(double sampleRate, int numChannels = 2, double initialLookaheadMs = -1.0)
94 {
95 if (!(sampleRate > 0.0)) return; // NaN-safe validity gate: keep previous state
96 prepared_ = false; // basic guarantee while (re)allocating
97
98 sampleRate_ = sampleRate;
99 // The true-peak detector state is a fixed kMaxChannels array; clamp so
100 // detection can never index it out of bounds for high channel counts.
101 numChannels_ = std::clamp(numChannels, 1, kMaxChannels);
102
103 // Caching inverse sample rate for fast math in hot paths
104 invSampleRate_ = T(1) / static_cast<T>(sampleRate_);
105
106 // Allocate for maximum possible lookahead to prevent RT-allocations
107 // later. The sample count is capped in double before the cast (a cast
108 // of an out-of-int-range double is undefined behaviour).
109 const double maxLaExact = sampleRate_ * kMaxLookaheadMs / 1000.0;
110 const int maxLookaheadSamples = static_cast<int>(std::min(maxLaExact, 1.0e7)) + 1;
111
112 // Use the clamped count: a degenerate numChannels (<= 0, or negative cast
113 // to size_t, or > kMaxChannels) must neither under-allocate (which would
114 // make processBlock index delayLines_ out of bounds) nor over-allocate.
115 delayLines_.resize(static_cast<size_t>(numChannels_));
116 for (auto& dl : delayLines_)
117 dl.prepare(maxLookaheadSamples * 2); // Double size for safety margin
118
119 if (std::isfinite(initialLookaheadMs) && initialLookaheadMs > 0.0)
120 lookaheadMs_.store(std::clamp(static_cast<T>(initialLookaheadMs),
121 T(0.5), static_cast<T>(kMaxLookaheadMs)),
122 std::memory_order_relaxed);
123 (void)lookaheadDirty_.exchange(false, std::memory_order_acquire);
125 lookaheadCurrent_ = static_cast<T>(lookaheadSamples_);
126
127 // Duration cap for the adaptive-release counter (2 seconds).
128 maxLimitSamples_ = static_cast<int>(std::min(sampleRate_ * 2.0, 1.0e9));
129
130 // Ceiling smoother setup
131 const T ceilDb = ceilingDb_.load(std::memory_order_relaxed);
132 lastCeilingDb_ = ceilDb;
133 const T ceilLinear = decibelsToGain(ceilDb);
134 ceilingSmooth_.prepare(sampleRate, 30.0);
135 ceilingSmooth_.reset(ceilLinear);
136
137 lastReleaseMs_ = std::max(releaseMs_.load(std::memory_order_relaxed), T(1));
139
140 reset();
141 prepared_ = true;
142 }
143
151 void prepare(const AudioSpec& spec)
152 {
153 prepare(spec.sampleRate, spec.numChannels);
154 }
155
167 void processBlock(AudioBufferView<T> buffer) noexcept
168 {
169 if (!prepared_) return;
170 DenormalGuard guard;
171
172 const int nCh = std::min(buffer.getNumChannels(), numChannels_);
173 const int nS = buffer.getNumSamples();
174
175 if (lookaheadDirty_.exchange(false, std::memory_order_acquire))
178
179 const bool isp = truePeakEnabled_.load(std::memory_order_relaxed);
180 const bool adaptive = adaptiveRelease_.load(std::memory_order_relaxed);
181 const bool safetyClip = safetyClipEnabled_.load(std::memory_order_relaxed);
182 const T relMs = std::max(releaseMs_.load(std::memory_order_relaxed), T(1));
183 const T lookTarget = static_cast<T>(lookaheadSamples_);
184
185 T* chData[kMaxChannels] = {};
186 for (int ch = 0; ch < nCh; ++ch)
187 chData[ch] = buffer.getChannel(ch);
188
189 for (int i = 0; i < nS; ++i)
190 {
191 T ceiling = ceilingSmooth_.getNextValue();
192 T peak = T(0);
193
194 // Live-change smoothing of the lookahead read offset: at most one
195 // sample of change per sample, so a setLookahead() during playback
196 // glides (brief micro pitch-shift) instead of clicking.
197 if (lookaheadCurrent_ < lookTarget) lookaheadCurrent_ += T(1);
198 else if (lookaheadCurrent_ > lookTarget) lookaheadCurrent_ -= T(1);
199 const int lookNow = static_cast<int>(lookaheadCurrent_);
200
201 // Phase 1: Peak detection and delay line push
202 for (int ch = 0; ch < nCh; ++ch)
203 {
204 T sample = chData[ch][i];
205 delayLines_[ch].push(sample);
206
207 T chPeak = isp ? truePeak_.processSample(sample, ch) : std::abs(sample);
208 if (chPeak > peak) peak = chPeak;
209 }
210
211 // Phase 2: Peak-hold over the lookahead window, then gain envelope.
212 // The gain reduction for a transient must persist until that transient
213 // reaches the (delayed) output lookaheadSamples_ later; otherwise an
214 // isolated peak would be output after the envelope has already released
215 // and could exceed the ceiling. Holding the detected peak for the
216 // look-ahead duration guarantees the brickwall without an instantaneous
217 // (clicky) attack; the one-pole attack still reaches ~99% over the window.
218 if (peak >= heldPeak_) { heldPeak_ = peak; peakHoldCounter_ = lookaheadSamples_; }
219 else if (peakHoldCounter_ > 0) { --peakHoldCounter_; }
220 else { heldPeak_ = peak; }
221
222 T targetGain = (heldPeak_ > ceiling) ? ceiling / heldPeak_ : T(1);
223 smoothGain(targetGain, adaptive, relMs);
224
225 // Phase 3: Apply gain, guarantee the ceiling, optional safety clip
226 for (int ch = 0; ch < nCh; ++ch)
227 {
228 T out = delayLines_[ch].read(lookNow) * currentGain_;
229
230 // Hard ceiling guarantee: the smoothed attack converges to
231 // ~99% of the target inside the lookahead window, leaving up
232 // to ~0.09 dB of residual overshoot. Clamping that residual is
233 // inaudible (it only ever trims the last 1%) and makes the
234 // brickwall contract exact.
235 out = std::clamp(out, -ceiling, ceiling);
236
237 if (safetyClip)
238 {
239 T clipCeil = std::min(kSafetyClipCeiling, ceiling);
240 if (std::abs(out) > clipCeil)
241 out = applySafetyClipper(out, clipCeil);
242 }
243
244 chData[ch][i] = out;
245 }
246 }
247 }
248
262 [[nodiscard]] T processSample(T input, int channel) noexcept
263 {
264 if (!prepared_) return input;
265 // Release-safe channel bound (delayLines_ is sized to numChannels_):
266 // out-of-range channels are an exact pass-through, no state touched.
267 if (channel < 0 || channel >= numChannels_) return input;
268
269 const bool isp = truePeakEnabled_.load(std::memory_order_relaxed);
270 const bool adaptive = adaptiveRelease_.load(std::memory_order_relaxed);
271 const bool safetyClip = safetyClipEnabled_.load(std::memory_order_relaxed);
272
273 if (channel == 0)
274 {
275 if (lookaheadDirty_.exchange(false, std::memory_order_acquire))
278 sampleCeiling_ = ceilingSmooth_.getNextValue();
279
280 const T lookTarget = static_cast<T>(lookaheadSamples_);
281 if (lookaheadCurrent_ < lookTarget) lookaheadCurrent_ += T(1);
282 else if (lookaheadCurrent_ > lookTarget) lookaheadCurrent_ -= T(1);
283 sampleLookNow_ = static_cast<int>(lookaheadCurrent_);
284 }
285 const T ceiling = sampleCeiling_;
286
287 delayLines_[channel].push(input);
288 const T chPeak = isp ? truePeak_.processSample(input, channel) : std::abs(input);
289
290 // Shared peak hold: any channel may raise it; the per-frame decay and
291 // the gain envelope advance on channel 0 only.
292 if (chPeak >= heldPeak_) { heldPeak_ = chPeak; peakHoldCounter_ = lookaheadSamples_; }
293 else if (channel == 0)
294 {
296 else heldPeak_ = chPeak;
297 }
298
299 if (channel == 0)
300 {
301 const T relMs = std::max(releaseMs_.load(std::memory_order_relaxed), T(1));
302 const T targetGain = (heldPeak_ > ceiling) ? ceiling / heldPeak_ : T(1);
303 smoothGain(targetGain, adaptive, relMs);
304 }
305
306 T out = delayLines_[channel].read(sampleLookNow_) * currentGain_;
307 out = std::clamp(out, -ceiling, ceiling); // exact brickwall contract
308
309 if (safetyClip)
310 {
311 const T clipCeil = std::min(kSafetyClipCeiling, ceiling);
312 if (std::abs(out) > clipCeil)
313 out = applySafetyClipper(out, clipCeil);
314 }
315 return out;
316 }
317
319 void reset() noexcept
320 {
321 for (auto& dl : delayLines_) dl.reset();
323 currentGain_ = T(1);
325 heldPeak_ = T(0);
327 lookaheadCurrent_ = static_cast<T>(lookaheadSamples_);
328 ceilingSmooth_.skip();
329 sampleCeiling_ = ceilingSmooth_.getCurrentValue();
331 }
332
333 // -- Level 1: Simple API ----------------------------------------------------
334
340 void setCeiling(T dB) noexcept
341 {
342 if (!std::isfinite(dB)) return;
343 ceilingDb_.store(dB, std::memory_order_relaxed);
344 }
345
346 // -- Level 2: Intermediate API ----------------------------------------------
347
353 void setRelease(T ms) noexcept
354 {
355 if (!std::isfinite(ms)) return;
356 releaseMs_.store(std::max(T(1), ms), std::memory_order_relaxed);
357 }
358
369 void setLookahead(T ms) noexcept
370 {
371 if (!std::isfinite(ms)) return;
372 lookaheadMs_.store(std::clamp(ms, T(0.5), static_cast<T>(kMaxLookaheadMs)),
373 std::memory_order_relaxed);
374 lookaheadDirty_.store(true, std::memory_order_release);
375 }
376
377 // -- Level 3: Expert API ----------------------------------------------------
378
380 void setTruePeak(bool enabled) noexcept { truePeakEnabled_.store(enabled, std::memory_order_relaxed); }
381
383 void setAdaptiveRelease(bool enabled) noexcept { adaptiveRelease_.store(enabled, std::memory_order_relaxed); }
384
392 void setSafetyClip(bool enabled) noexcept { safetyClipEnabled_.store(enabled, std::memory_order_relaxed); }
393
394 // Getters
395 [[nodiscard]] bool isTruePeakEnabled() const noexcept { return truePeakEnabled_.load(std::memory_order_relaxed); }
396 [[nodiscard]] bool isAdaptiveReleaseEnabled() const noexcept { return adaptiveRelease_.load(std::memory_order_relaxed); }
397 [[nodiscard]] bool isSafetyClipEnabled() const noexcept { return safetyClipEnabled_.load(std::memory_order_relaxed); }
398 [[nodiscard]] T getLookahead() const noexcept { return lookaheadMs_.load(std::memory_order_relaxed); }
399
407 [[nodiscard]] int getLatency() const noexcept
408 {
409 return lookaheadSamplesFor(lookaheadMs_.load(std::memory_order_relaxed));
410 }
411
413 [[nodiscard]] T getGainReductionDb() const noexcept { return gainToDecibels(currentGain_); }
414
415
417 [[nodiscard]] std::vector<uint8_t> getState() const
418 {
419 StateWriter w(stateId("LIMT"), 1);
420 w.write("ceiling", static_cast<float>(ceilingDb_.load(std::memory_order_relaxed)));
421 w.write("release", static_cast<float>(releaseMs_.load(std::memory_order_relaxed)));
422 w.write("lookahead", static_cast<float>(lookaheadMs_.load(std::memory_order_relaxed)));
423 w.write("truePeak", truePeakEnabled_.load(std::memory_order_relaxed));
424 w.write("adaptive", adaptiveRelease_.load(std::memory_order_relaxed));
425 w.write("safetyClip", safetyClipEnabled_.load(std::memory_order_relaxed));
426 return w.blob();
427 }
428
430 bool setState(const uint8_t* data, size_t size)
431 {
432 StateReader r(data, size);
433 if (!r.isValid() || r.processorId() != stateId("LIMT")) return false;
434 setCeiling(static_cast<T>(r.read("ceiling", -0.3f)));
435 setRelease(static_cast<T>(r.read("release", 100.0f)));
436 setLookahead(static_cast<T>(r.read("lookahead", 2.0f)));
437 setTruePeak(r.read("truePeak", false));
438 setAdaptiveRelease(r.read("adaptive", false));
439 setSafetyClip(r.read("safetyClip", false));
440 return true;
441 }
442
443protected:
444 static constexpr int kMaxChannels = 16;
445 static constexpr double kMaxLookaheadMs = 10.0;
446 static constexpr T kSafetyClipCeiling = T(0.96605);
447
448 // Shared ITU-R BS.1770-4 true-peak detector (Core/TruePeakDetector.h).
450
454 [[nodiscard]] int lookaheadSamplesFor(T ms) const noexcept
455 {
456 const double clampedMs = std::clamp(static_cast<double>(ms), 0.5, kMaxLookaheadMs);
457 const double exact = sampleRate_ * clampedMs / 1000.0;
458 return std::max(1, static_cast<int>(std::min(exact, 1.0e7)));
459 }
460
462 inline void applyLookaheadTarget() noexcept
463 {
464 lookaheadSamples_ = lookaheadSamplesFor(lookaheadMs_.load(std::memory_order_relaxed));
465
466 // Calculate an attack coefficient that guarantees reaching 99% of target gain
467 // exactly within the lookahead window to prevent transient clipping.
468 // Formula: alpha = 1 - exp(-ln(100) / samples)
469 attackCoeff_ = T(1) - std::exp(T(-4.60517) / static_cast<T>(lookaheadSamples_));
470 }
471
472 // Fast-path synchronization for atomic variables. The linear ceiling is
473 // only recomputed when the dB parameter actually changed (skips the pow).
474 inline void syncParameters() noexcept
475 {
476 const T ceilDb = ceilingDb_.load(std::memory_order_relaxed);
477 if (ceilDb != lastCeilingDb_)
478 {
479 lastCeilingDb_ = ceilDb;
480 ceilingSmooth_.setTargetValue(decibelsToGain(ceilDb));
481 }
482
483 const T relMs = std::max(releaseMs_.load(std::memory_order_relaxed), T(1));
484 if (relMs != lastReleaseMs_)
485 {
486 lastReleaseMs_ = relMs;
488 }
489 }
490
491 inline void updateReleaseCoefficient() noexcept
492 {
493 if (sampleRate_ > 0)
494 releaseCoeff_ = T(1) - std::exp(T(-1) / (static_cast<T>(sampleRate_) * lastReleaseMs_ / T(1000)));
495 }
496
497 inline void smoothGain(T targetGain, bool adaptive, T relMs) noexcept
498 {
499 if (targetGain < currentGain_)
500 {
501 // Smoothed attack to prevent discontinuous clicks on transients
502 currentGain_ += attackCoeff_ * (targetGain - currentGain_);
503
504 // Limit duration to max 2 seconds to prevent integer overflow
506 }
507 else
508 {
509 T coeff;
510 if (adaptive)
511 {
512 T baseFactor = T(1);
513 if (limitingDuration_ > 0)
514 {
515 T durationMs = static_cast<T>(limitingDuration_) * T(1000) * invSampleRate_;
516 baseFactor = T(1) + std::min(durationMs / T(100), T(2));
517 }
518 T adaptedRelease = relMs * baseFactor;
519
520 // Fast path for exp() approximation: 1 / (1 + fs * tau_seconds)
521 // Avoids brutal CPU spike of std::exp() in the audio thread loop
522 coeff = T(1) / (T(1) + (static_cast<T>(sampleRate_) * adaptedRelease / T(1000)));
523 }
524 else
525 {
526 coeff = releaseCoeff_;
527 }
528
529 currentGain_ += coeff * (targetGain - currentGain_);
530 if (currentGain_ > T(1)) currentGain_ = T(1);
531 if (currentGain_ > T(0.999)) limitingDuration_ = 0;
532 }
533 }
534
537 [[nodiscard]] inline T applySafetyClipper(T out, T clipCeil) const noexcept
538 {
539 T sign = (out >= T(0)) ? T(1) : T(-1);
540 T excess = std::abs(out) - clipCeil;
541 T blend = T(1) / (T(1) + excess * T(10));
542 out = sign * (clipCeil * blend + std::abs(out) * (T(1) - blend));
543
544 const T hardCeil = clipCeil * T(1.05);
545 if (std::abs(out) > hardCeil)
546 out = std::clamp(out, -hardCeil, hardCeil);
547
548 return out;
549 }
550
551 bool prepared_ = false;
552 double sampleRate_ = 48000.0;
553 T invSampleRate_ = T(1.0 / 48000.0);
556 int maxLimitSamples_ = 96000;
557
558 std::atomic<T> ceilingDb_ { T(-0.3) };
559 std::atomic<T> releaseMs_ { T(100) };
560 std::atomic<T> lookaheadMs_ { T(2) };
561 std::atomic<bool> lookaheadDirty_ { false };
562 std::atomic<bool> truePeakEnabled_ { false };
563 std::atomic<bool> adaptiveRelease_ { false };
564 std::atomic<bool> safetyClipEnabled_ { false };
565
567 T lastCeilingDb_ = T(-0.3);
568
570 T attackCoeff_ = T(1);
571 T lastReleaseMs_ = T(-1);
572
573 T currentGain_ = T(1);
576
577 // Look-ahead peak hold (brickwall guarantee): holds the detected peak for
578 // lookaheadSamples_ so the gain stays reduced until the peak is output.
579 T heldPeak_ = T(0);
581
582 // Per-sample path caches (advanced on channel 0, reused by later channels).
583 T sampleCeiling_ = T(0.96605);
585
586 std::vector<RingBuffer<T>> delayLines_;
587};
588
589} // namespace dspark
Non-owning view over audio channel data.
Definition AudioBuffer.h:50
RAII scope guard to disable denormalised (subnormal) floating-point numbers.
High-performance brickwall lookahead limiter.
Definition Limiter.h:71
void setLookahead(T ms) noexcept
Sets the lookahead time dynamically.
Definition Limiter.h:369
void prepare(double sampleRate, int numChannels=2, double initialLookaheadMs=-1.0)
Allocates memory and prepares the limiter for processing.
Definition Limiter.h:93
void setRelease(T ms) noexcept
Sets the base release time.
Definition Limiter.h:353
bool isAdaptiveReleaseEnabled() const noexcept
Definition Limiter.h:396
std::atomic< T > lookaheadMs_
Definition Limiter.h:560
T processSample(T input, int channel) noexcept
Processes a single sample of one channel.
Definition Limiter.h:262
T getGainReductionDb() const noexcept
Current gain reduction in dB (metering; unsynchronized read).
Definition Limiter.h:413
T lookaheadCurrent_
Smoothed read offset (glides on live changes).
Definition Limiter.h:575
bool setState(const uint8_t *data, size_t size)
Restores parameters from a blob (tolerant; rejects foreign ids).
Definition Limiter.h:430
~Limiter()=default
void prepare(const AudioSpec &spec)
Prepares from AudioSpec (unified API).
Definition Limiter.h:151
int maxLimitSamples_
Adaptive-release duration cap (2 s).
Definition Limiter.h:556
void setSafetyClip(bool enabled) noexcept
Enables the post-limiter soft-knee safety clipper. RT-Safe.
Definition Limiter.h:392
std::atomic< bool > lookaheadDirty_
Definition Limiter.h:561
void syncParameters() noexcept
Definition Limiter.h:474
std::atomic< bool > truePeakEnabled_
Definition Limiter.h:562
std::vector< uint8_t > getState() const
Serializes the parameter state (setup/UI threads; allocates).
Definition Limiter.h:417
int getLatency() const noexcept
Reports the processing latency (the lookahead) in samples.
Definition Limiter.h:407
void smoothGain(T targetGain, bool adaptive, T relMs) noexcept
Definition Limiter.h:497
int lookaheadSamplesFor(T ms) const noexcept
Definition Limiter.h:454
std::atomic< bool > safetyClipEnabled_
Definition Limiter.h:564
void reset() noexcept
Resets the internal state (delays, gain reduction). RT-Safe.
Definition Limiter.h:319
void updateReleaseCoefficient() noexcept
Definition Limiter.h:491
SmoothedValue< T > ceilingSmooth_
Definition Limiter.h:566
int lookaheadSamples_
Audio-side lookahead (consumed from lookaheadMs_).
Definition Limiter.h:555
T applySafetyClipper(T out, T clipCeil) const noexcept
Definition Limiter.h:537
void applyLookaheadTarget() noexcept
Definition Limiter.h:462
void setTruePeak(bool enabled) noexcept
Enables 4x oversampled ISP true-peak detection. RT-Safe.
Definition Limiter.h:380
bool isSafetyClipEnabled() const noexcept
Definition Limiter.h:397
std::vector< RingBuffer< T > > delayLines_
Definition Limiter.h:586
std::atomic< T > releaseMs_
Definition Limiter.h:559
void setAdaptiveRelease(bool enabled) noexcept
Enables program-dependent adaptive release. RT-Safe.
Definition Limiter.h:383
void processBlock(AudioBufferView< T > buffer) noexcept
Processes an AudioBufferView in-place.
Definition Limiter.h:167
T getLookahead() const noexcept
Definition Limiter.h:398
std::atomic< bool > adaptiveRelease_
Definition Limiter.h:563
bool isTruePeakEnabled() const noexcept
Definition Limiter.h:395
static constexpr T kSafetyClipCeiling
-0.3 dBFS
Definition Limiter.h:446
static constexpr double kMaxLookaheadMs
Definition Limiter.h:445
static constexpr int kMaxChannels
Definition Limiter.h:444
std::atomic< T > ceilingDb_
Definition Limiter.h:558
void setCeiling(T dB) noexcept
Sets the absolute output ceiling.
Definition Limiter.h:340
double sampleRate_
Definition Limiter.h:552
T lastCeilingDb_
Change detector for the ceiling pow skip.
Definition Limiter.h:567
int peakHoldCounter_
Definition Limiter.h:580
TruePeakDetector< T, kMaxChannels > truePeak_
Definition Limiter.h:449
int limitingDuration_
Definition Limiter.h:574
Zero-allocation parameter smoother for real-time audio.
Tolerant reader: missing keys yield defaults, unknown keys are skipped.
Definition StateBlob.h:160
float read(const char *key, float defaultValue) const
Reads a float, or defaultValue when the key is absent.
Definition StateBlob.h:203
bool isValid() const noexcept
Definition StateBlob.h:198
uint32_t processorId() const noexcept
Definition StateBlob.h:199
Serializes key/value parameters into a versioned blob.
Definition StateBlob.h:52
std::vector< uint8_t > blob() const
Finalizes and returns the blob.
Definition StateBlob.h:104
void write(const char *key, float value)
Writes a float parameter.
Definition StateBlob.h:70
Per-channel 4x-oversampled inter-sample peak estimator.
T processSample(T sample, int channel) noexcept
Feeds one sample and returns the local true-peak estimate.
void reset() noexcept
Clears all channel histories. Safe on the audio thread.
Main namespace for the DSPark framework.
T decibelsToGain(T dB, T minusInfinityDb=T(-100)) noexcept
Converts a value in decibels to linear gain.
Definition DspMath.h:65
T gainToDecibels(T gain, T minusInfinityDb=T(-100)) noexcept
Converts a linear gain value to decibels.
Definition DspMath.h:78
constexpr uint32_t stateId(const char(&tag)[5]) noexcept
Builds a FOURCC processor id, e.g. dspark::stateId("COMP").
Definition StateBlob.h:639
Describes the audio environment for a DSP processor.
Definition AudioSpec.h:35
int numChannels
Number of audio channels (e.g., 1 = mono, 2 = stereo).
Definition AudioSpec.h:56
double sampleRate
Sample rate in Hz.
Definition AudioSpec.h:43