Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
lofi_chain.hpp
Go to the documentation of this file.
1#pragma once
2
6
7#include <algorithm>
8#include <cmath>
9#include <cstdint>
10#include <limits>
11#include <type_traits>
12
13namespace pulp::signal {
14
22inline double quantize_bits(double x, double bits) {
23 if (bits >= 24.0) return x;
24 const double levels = std::pow(2.0, std::max(bits, 1.0) - 1.0);
25 return std::round(x * levels) / levels;
26}
27
39inline double dead_zone_saturate(double x, double dead_zone) {
40 const double dz = std::clamp(dead_zone, 0.0, 0.9);
41 const double magnitude = std::fabs(x);
42 if (magnitude <= dz) return 0.0;
43 const double rescaled = (magnitude - dz) / (1.0 - dz);
44 return std::copysign(std::tanh(1.5 * rescaled), x);
45}
46
55
81template <typename Sample = double>
82inline Sample floor_shape(Sample x, std::type_identity_t<Sample> threshold, FloorCurve curve) {
83 static_assert(std::is_floating_point_v<Sample>);
84 const Sample limit = std::clamp(threshold, Sample{0}, Sample{0.9});
85 const Sample magnitude = std::fabs(x);
86 if (magnitude <= limit)
87 return Sample{0};
88 const Sample normalized = std::min((magnitude - limit) / (Sample{1} - limit), Sample{1});
89 const Sample shaped = curve == FloorCurve::smoothstep
90 ? normalized * normalized * (Sample{3} - Sample{2} * normalized)
91 : normalized;
92 return std::copysign(shaped, x);
93}
94
112template <typename SampleType = float>
114public:
115 static constexpr std::uint32_t default_seed = 0x6A09E667u;
116
117 void set_sample_rate(double sr) {
118 sample_rate_ = sr > 0.0 ? sr : sample_rate_;
119 update();
120 }
121
127 void set_hold_rate_hz(double hz) {
128 hold_rate_ = std::max(hz, 1.0);
129 update();
130 }
131
133 void set_jitter(double amount) {
134 jitter_ = std::clamp(amount, 0.0, 1.0);
135 }
136
142 void set_smoothing(double amount) {
143 smoothing_ = std::clamp(amount, 0.0, 1.0);
144 update();
145 }
146
147 void set_seed(std::uint32_t seed) { seed_ = seed == 0 ? default_seed : seed; }
148
152 if (sample_rate_ != other.sample_rate_)
153 set_sample_rate(other.sample_rate_);
154 if (hold_rate_ != other.hold_rate_)
155 set_hold_rate_hz(other.hold_rate_);
156 if (jitter_ != other.jitter_)
157 set_jitter(other.jitter_);
158 if (smoothing_ != other.smoothing_)
159 set_smoothing(other.smoothing_);
160 seed_ = other.seed_;
161 }
162
163 void reset() {
164 phase_ = 1.0; // latch on the first sample rather than emitting a zero
165 held_ = 0.0;
166 output_level_ = 0.0;
167 smoother_.reset();
168 rng_ = seed_;
169 }
170
171 bool has_tail() const noexcept {
172 return (!hold_bypassed_ && std::fabs(held_) > 1.0e-12) ||
173 (!bypass_smoothing_ && output_level_ > 1.0e-8);
174 }
175
176 SampleType process(SampleType input) {
177 // Latch before advancing. Advancing first would make the very first
178 // hold one sample short, because `reset()` parks the phase at the
179 // latch point so the first sample is captured rather than emitted as
180 // a leftover zero.
181 if (phase_ >= 1.0) {
182 phase_ -= std::floor(phase_);
183 held_ = static_cast<double>(input);
184 }
185 phase_ += step_ * (1.0 + jitter_ * next_jitter());
186 const SampleType output =
187 bypass_smoothing_
188 ? static_cast<SampleType>(held_)
189 : smoother_.process_lowpass(static_cast<SampleType>(held_));
190 output_level_ = std::fabs(static_cast<double>(output));
191 return output;
192 }
193
194private:
195 double next_jitter() {
196 rng_ ^= rng_ << 13;
197 rng_ ^= rng_ >> 17;
198 rng_ ^= rng_ << 5;
199 return static_cast<double>(rng_) * (1.0 / 2147483648.0) - 1.0;
200 }
201
202 void update() {
203 const double rate = std::min(hold_rate_, sample_rate_);
204 step_ = rate / sample_rate_;
205 hold_bypassed_ = rate >= sample_rate_;
206 smoother_.prepare(static_cast<SampleType>(sample_rate_));
207 bypass_smoothing_ = smoothing_ <= 0.0;
208 if (!bypass_smoothing_) {
209 // Sweep the corner from the host Nyquist down to the hold rate's
210 // Nyquist, so smoothing=1 thins the images the most and smoothing
211 // just above zero barely touches the top of the band.
212 const double top = 0.5 * sample_rate_;
213 const double bottom = 0.5 * rate;
214 smoother_.set_cutoff(static_cast<SampleType>(top * std::pow(bottom / top, smoothing_)));
215 }
216 }
217
218 double sample_rate_ = 44100.0;
219 double hold_rate_ = std::numeric_limits<double>::max();
220 double jitter_ = 0.0;
221 double smoothing_ = 0.0;
222
223 double step_ = 1.0;
224 double phase_ = 1.0;
225 double held_ = 0.0;
226 double output_level_ = 0.0;
227 TptFilterT<SampleType> smoother_;
228 bool hold_bypassed_ = true;
229 bool bypass_smoothing_ = true;
230
231 std::uint32_t seed_ = default_seed;
232 std::uint32_t rng_ = default_seed;
233};
234
250template <typename SampleType = float>
252public:
253 void set_sample_rate(double sr) { reducer_.set_sample_rate(sr); }
254
256 void set_bits(double bits) {
257 bits_ = std::isfinite(bits) ? std::clamp(bits, 1.0, 24.0) : 24.0;
258 quantizer_.set_bits(static_cast<SampleType>(bits_));
259 }
260
262 void set_dither_mode(DitherMode mode) { quantizer_.set_dither_mode(mode); }
263
266 quantizer_.set_noise_shaping(order);
267 }
268
271 void set_hold_rate_hz(double hz) { reducer_.set_hold_rate_hz(hz); }
272
273 void set_jitter(double amount) { reducer_.set_jitter(amount); }
274
275 void set_smoothing(double amount) { reducer_.set_smoothing(amount); }
276
278 void set_dead_zone(double amount) { dead_zone_ = std::clamp(amount, 0.0, 0.9); }
279
280 void set_seed(std::uint32_t seed) {
281 reducer_.set_seed(seed);
282 quantizer_.set_seed(seed);
283 }
284
288 bits_ = other.bits_;
289 dead_zone_ = other.dead_zone_;
290 reducer_.sync_configuration_from(other.reducer_);
291 quantizer_.sync_configuration_from(other.quantizer_);
292 }
293
294 void reset() {
295 reducer_.reset();
296 quantizer_.reset();
297 }
298 bool has_tail() const noexcept { return reducer_.has_tail(); }
299
300 SampleType process(SampleType input) {
301 double x;
302 if (quantizer_.dither_mode() == DitherMode::none &&
303 quantizer_.noise_shaping() == NoiseShapingOrder::none) {
304 // Preserve the original double-precision arithmetic exactly for
305 // callers that do not opt into the output-correctness policy.
306 x = quantize_bits(static_cast<double>(input), bits_);
307 } else {
308 x = static_cast<double>(quantizer_.process(input));
309 }
310 x = static_cast<double>(reducer_.process(static_cast<SampleType>(x)));
311 if (dead_zone_ > 0.0) x = dead_zone_saturate(x, dead_zone_);
312 return static_cast<SampleType>(x);
313 }
314
315 void process(SampleType* buffer, int num_samples) {
316 for (int i = 0; i < num_samples; ++i) buffer[i] = process(buffer[i]);
317 }
318
319private:
322 double bits_ = 24.0;
323 double dead_zone_ = 0.0;
324};
325
330
331} // namespace pulp::signal
Stateful deterministic wrapper for per-sample and block processing.
Definition dither.hpp:131
The three lo-fi stages in the order the hardware they model applies them: quantise the word,...
Definition lofi_chain.hpp:251
void set_dead_zone(double amount)
Fraction of full scale the output stage cannot represent, 0 to 0.9.
Definition lofi_chain.hpp:278
void set_smoothing(double amount)
Definition lofi_chain.hpp:275
void process(SampleType *buffer, int num_samples)
Definition lofi_chain.hpp:315
bool has_tail() const noexcept
Definition lofi_chain.hpp:298
void set_hold_rate_hz(double hz)
Hold clock in Hz.
Definition lofi_chain.hpp:271
void reset()
Definition lofi_chain.hpp:294
void set_noise_shaping(NoiseShapingOrder order)
Error feedback is opt-in and may be used with or without dither.
Definition lofi_chain.hpp:265
void set_seed(std::uint32_t seed)
Definition lofi_chain.hpp:280
SampleType process(SampleType input)
Definition lofi_chain.hpp:300
void set_sample_rate(double sr)
Definition lofi_chain.hpp:253
void sync_configuration_from(const LofiChainT &other)
Mirror parameter configuration while preserving this chain's independent reducer/filter state.
Definition lofi_chain.hpp:287
void set_jitter(double amount)
Definition lofi_chain.hpp:273
void set_bits(double bits)
Word length in bits. 24 or above bypasses the quantiser.
Definition lofi_chain.hpp:256
void set_dither_mode(DitherMode mode)
Quantizer dither is opt-in so existing chains remain bit-exact.
Definition lofi_chain.hpp:262
Sample-and-hold rate reducer with clock jitter and a reconstruction filter.
Definition lofi_chain.hpp:113
void set_jitter(double amount)
Clock jitter as a fraction of the hold period, 0 to 1.
Definition lofi_chain.hpp:133
void reset()
Definition lofi_chain.hpp:163
void set_sample_rate(double sr)
Definition lofi_chain.hpp:117
void sync_configuration_from(const SampleRateReducerT &other)
Mirror another reducer's parameter configuration without copying its held sample, clock phase,...
Definition lofi_chain.hpp:151
SampleType process(SampleType input)
Definition lofi_chain.hpp:176
void set_seed(std::uint32_t seed)
Definition lofi_chain.hpp:147
bool has_tail() const noexcept
Definition lofi_chain.hpp:171
void set_hold_rate_hz(double hz)
Rate the held samples are clocked at, in Hz.
Definition lofi_chain.hpp:127
static constexpr std::uint32_t default_seed
Definition lofi_chain.hpp:115
void set_smoothing(double amount)
Strength of the reconstruction lowpass, 0 (bypassed, raw images) to 1 (corner at the hold rate's Nyqu...
Definition lofi_chain.hpp:142
Deterministic TPDF dither and bounded error-feedback quantization.
Definition additive_bank.hpp:218
LofiChainT< float > LofiChain
Definition lofi_chain.hpp:328
@ buffer
A borrowed static buffer with a traversable playhead.
Definition granular.hpp:110
NoiseShapingOrder
Definition dither.hpp:21
@ none
Definition dither.hpp:22
FloorCurve
The curve applied above a floor threshold by floor_shape.
Definition lofi_chain.hpp:48
@ smoothstep
Smoothstep, which leaves the threshold with zero slope so the onset of audibility is gradual rather t...
Definition lofi_chain.hpp:53
float smoothstep(float p)
Cubic smoothstep, 3t^2 - 2t^3.
Definition mod_tools.hpp:75
SampleRateReducerT< double > SampleRateReducer64
Definition lofi_chain.hpp:327
double dead_zone_saturate(double x, double dead_zone)
A dead-zone saturator: a hard gap around zero, then a soft-limited slope.
Definition lofi_chain.hpp:39
Sample floor_shape(Sample x, std::type_identity_t< Sample > threshold, FloorCurve curve)
A noise-floor shaper: silence below threshold, rescaled above it.
Definition lofi_chain.hpp:82
LofiChainT< double > LofiChain64
Definition lofi_chain.hpp:329
@ levels
Definition multi_channel_meter.hpp:48
@ i
Definition formant_filter_bank.hpp:20
double quantize_bits(double x, double bits)
Quantises to bits of resolution, mid-tread, symmetric about zero.
Definition lofi_chain.hpp:22
DitherMode
Definition dither.hpp:16
@ none
Definition dither.hpp:17
@ linear
2-point. Cheapest; audible imaging when pitching up.
Definition granular.hpp:133
SampleRateReducerT< float > SampleRateReducer
Definition lofi_chain.hpp:326
First-order topology-preserving transform (TPT) filter.