Sound Byte Libs 29c5ff3
C++ firmware library for audio applications on 32-bit ARM Cortex-M processors
Loading...
Searching...
No Matches
hysteresis_quantizer.hpp
Go to the documentation of this file.
1// sbl/dsp/hysteresis_quantizer.hpp — Stepped quantizer with dead zones
2//
3// Maps a continuous input (0-65535) to discrete steps with hysteresis
4// at each boundary. Once settled on a step, the input must move past
5// the boundary by a configurable margin before the step changes.
6// This eliminates jitter when a knob sits right at a transition point.
7//
8// Hysteresis is specified as a fraction of step width (0-255 maps to
9// 0-100% of step width, default 26 ≈ 10%). Values above ~50% cause
10// the dead zones to overlap, which isn't harmful but makes transitions
11// feel sluggish.
12//
13// Inspired by Mutable Instruments stmlib (MIT).
14//
15// Usage:
16// sbl::dsp::HysteresisQuantizer q;
17// int step = q.process(adc_value, 8); // 8 steps, returns 0-7
18
19#pragma once
20
21#include <cstdint>
22
23namespace sbl::dsp {
24
26public:
27 /// Process a raw value into a quantized step index.
28 /// @param value Input value (0-65535, typically from ADC or pot).
29 /// @param n_steps Number of output steps (2-256).
30 /// @return Step index (0 to n_steps-1).
31 int process(uint16_t value, uint8_t n_steps) {
32 if (n_steps < 2) return 0;
33
34 // Raw quantization: which step would this value fall in?
35 int raw_step = (static_cast<uint32_t>(value) * n_steps) >> 16;
36 if (raw_step >= n_steps) raw_step = n_steps - 1;
37
38 // First call or step count changed — snap immediately
39 if (!initialized_ || n_steps != last_n_steps_) {
40 step_ = raw_step;
41 initialized_ = true;
42 last_n_steps_ = n_steps;
43 return step_;
44 }
45
46 // Same step — no change needed
47 if (raw_step == step_) return step_;
48
49 // Different step — check if we've crossed the hysteresis band.
50 // Step width in the 0-65535 space:
51 uint32_t step_width = 65536u / n_steps;
52 // Hysteresis margin: fraction of step width (hysteresis_/256)
53 uint32_t margin = (step_width * hysteresis_) >> 8;
54
55 if (raw_step > step_) {
56 // Moving up: value must exceed upper boundary + margin
57 uint32_t threshold = static_cast<uint32_t>(step_ + 1) * step_width + margin;
58 if (value >= threshold) {
59 step_ = raw_step;
60 }
61 } else {
62 // Moving down: value must go below lower boundary - margin
63 uint32_t boundary = static_cast<uint32_t>(step_) * step_width;
64 if (boundary >= margin && value <= boundary - margin) {
65 step_ = raw_step;
66 } else if (boundary < margin && value == 0) {
67 step_ = 0;
68 }
69 }
70
71 return step_;
72 }
73
74 /// Current quantized step (valid after first process() call).
75 int step() const { return step_; }
76
77 /// Set hysteresis amount (0-255). 0 = no hysteresis, 26 ≈ 10%, 128 = 50%.
78 void set_hysteresis(uint8_t h) { hysteresis_ = h; }
79
80 /// Reset — next process() call will snap to the raw value.
81 void reset() { initialized_ = false; }
82
83private:
84 int step_ = 0;
85 uint8_t hysteresis_ = 26; // ~10% of step width
86 uint8_t last_n_steps_ = 0;
87 bool initialized_ = false;
88};
89
90} // namespace sbl::dsp
int step() const
Current quantized step (valid after first process() call).
void reset()
Reset — next process() call will snap to the raw value.
void set_hysteresis(uint8_t h)
Set hysteresis amount (0-255). 0 = no hysteresis, 26 ≈ 10%, 128 = 50%.
int process(uint16_t value, uint8_t n_steps)
DSP atoms for audio signal processing.
Definition allpass.hpp:22