DSPark 1.6.1
Header-only audio DSP framework in pure C++20 — zero dependencies
Loading...
Searching...
No Matches
Dither.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 <algorithm>
19#include <array>
20#include <atomic>
21#include <cmath>
22#include <cstdint>
23#include <limits>
24#include <type_traits>
25
26namespace dspark {
27
54template <typename T>
55class Dither
56{
57 static_assert(std::is_floating_point_v<T>, "Dither requires a floating-point sample type");
58
59public:
69 explicit Dither(int targetBits = 16, bool noiseShaping = false) noexcept
70 : noiseShaping_(noiseShaping)
71 {
72 static std::atomic<uint32_t> seedGenerator{ 0x193A6B54u };
73 // Increment by golden ratio to ensure diverse initial states across instances
74 rngState_ = seedGenerator.fetch_add(0x9E3779B9u, std::memory_order_relaxed);
75 // xorshift32 has a fixed point at 0: if the rolling seed ever lands
76 // there, the dither would fall silent for that instance.
77 if (rngState_ == 0) rngState_ = 1;
78
79 setTargetBitDepth(targetBits);
80 }
81
86 void setTargetBitDepth(int bits) noexcept
87 {
88 // float (32-bit IEEE 754) only has 24 bits of mantissa precision.
89 constexpr int maxBits = (sizeof(T) == 4) ? 24 : 32;
90 targetBits_ = std::clamp(bits, 8, maxBits);
91
92 // Levels per unit: 2^(N-1). The representable grid is asymmetric,
93 // [-levels, levels - 1], mirroring the signed-integer formats.
94 int64_t levels = int64_t(1) << (targetBits_ - 1);
95
96 quantScale_ = static_cast<T>(levels);
97 quantStep_ = T(1) / quantScale_;
98 loLevel_ = -quantScale_;
99 hiLevel_ = quantScale_ - T(1);
100 // Feedback cap: a legitimate shaping error never exceeds 1.5 LSB
101 // (TPDF within (-1, 1] LSB + quantiser within +-0.5 LSB); see
102 // processSampleInternal for why the cap is needed at all.
103 errorCap_ = T(2) * quantStep_;
104 }
105
107 void setNoiseShaping(bool enabled) noexcept { noiseShaping_ = enabled; }
108
110 void reset() noexcept
111 {
112 std::fill(errorState_.begin(), errorState_.end(), T(0));
113 }
114
122 [[nodiscard]] T processSample(T input, int channel = 0) noexcept
123 {
124 if (noiseShaping_ && channel >= 0 && channel < kMaxChannels)
125 return processSampleInternal<true>(input, channel);
126 else
127 return processSampleInternal<false>(input, channel);
128 }
129
140 void processBlock(T* data, int numSamples, int channel = 0) noexcept
141 {
142 if (noiseShaping_ && channel >= 0 && channel < kMaxChannels)
143 {
144 for (int i = 0; i < numSamples; ++i)
145 data[i] = processSampleInternal<true>(data[i], channel);
146 }
147 else
148 {
149 for (int i = 0; i < numSamples; ++i)
150 data[i] = processSampleInternal<false>(data[i], channel);
151 }
152 }
153
154 [[nodiscard]] int getTargetBitDepth() const noexcept { return targetBits_; }
155 [[nodiscard]] T getQuantisationStep() const noexcept { return quantStep_; }
156
157private:
158 static constexpr int kMaxChannels = 16;
159
163 template <bool ApplyNoiseShaping>
164 [[nodiscard]] inline T processSampleInternal(T input, int channel) noexcept
165 {
166 // 2 LSB peak-to-peak TPDF noise
167 const T noise = (nextRandom() + nextRandom()) * quantStep_;
168
169 T preQuantise = input;
170
171 if constexpr (ApplyNoiseShaping)
172 {
173 preQuantise -= errorState_[static_cast<size_t>(channel)];
174 }
175
176 // Add dither to the signal (w[n] = v[n] + d[n])
177 const T dithered = preQuantise + noise;
178
179 // Quantise on the integer level grid and clamp to the representable
180 // range of the target depth. Ties are broken by the dither, so the
181 // rounding mode of nearbyint (default round-to-nearest) is fine and
182 // compiles to a single instruction on SSE4.1/NEON.
183 const T level = std::clamp(std::nearbyint(dithered * quantScale_), loLevel_, hiLevel_);
184 const T quantised = level * quantStep_;
185
186 if constexpr (ApplyNoiseShaping)
187 {
188 // Feed back the TOTAL requantisation error e[n] = y[n] - v[n]
189 // (dither + quantiser), the classic error-feedback structure
190 // with the dither inside the loop: the whole noise floor is
191 // shaped by (1 - z^-1), not just the quantiser error.
192 // The cap bounds the loop when the level clamp engages (input at
193 // or beyond positive full scale): without it the clip error
194 // accumulates sample after sample and pins the output at full
195 // scale long after the overload has passed.
196 const T err = quantised - preQuantise;
197 errorState_[static_cast<size_t>(channel)] = std::clamp(err, -errorCap_, errorCap_);
198 }
199
200 return quantised;
201 }
202
207 [[nodiscard]] inline T nextRandom() noexcept
208 {
209 rngState_ ^= rngState_ << 13;
210 rngState_ ^= rngState_ >> 17;
211 rngState_ ^= rngState_ << 5;
212
213 constexpr T scale = T(1) / static_cast<T>(std::numeric_limits<uint32_t>::max());
214 return static_cast<T>(rngState_) * scale - T(0.5);
215 }
216
217 int targetBits_ = 16;
218 T quantStep_ = T(1) / T(32768);
219 T quantScale_ = T(32768);
220 T loLevel_ = T(-32768);
221 T hiLevel_ = T(32767);
222 T errorCap_ = T(2) / T(32768);
223 bool noiseShaping_ = false;
224 uint32_t rngState_;
225 std::array<T, kMaxChannels> errorState_{};
226};
227
228} // namespace dspark
TPDF dithering processor with optional 1st-order noise shaping.
Definition Dither.h:56
T processSample(T input, int channel=0) noexcept
Applies TPDF dither and quantises a single sample.
Definition Dither.h:122
T getQuantisationStep() const noexcept
Definition Dither.h:155
int getTargetBitDepth() const noexcept
Definition Dither.h:154
Dither(int targetBits=16, bool noiseShaping=false) noexcept
Constructs a dithering processor.
Definition Dither.h:69
void reset() noexcept
Resets the noise shaping state. Call this when playback stops or flushes.
Definition Dither.h:110
void setNoiseShaping(bool enabled) noexcept
Enables or disables 1st-order noise shaping.
Definition Dither.h:107
void processBlock(T *data, int numSamples, int channel=0) noexcept
Applies dithering to an entire audio buffer in-place.
Definition Dither.h:140
void setTargetBitDepth(int bits) noexcept
Sets the target bit depth and recalculates scaling factors.
Definition Dither.h:86
Main namespace for the DSPark framework.