Sound Byte Libs 29c5ff3
C++ firmware library for audio applications on 32-bit ARM Cortex-M processors
Loading...
Searching...
No Matches
pot.hpp
Go to the documentation of this file.
1/**
2 * @file pot.hpp
3 * @brief Potentiometer component — smoothed ADC with deadband + change detection
4 * @ingroup components
5 *
6 * Pot wraps CvInput (EWMA smoothing) and adds deadband suppression so that
7 * small ADC noise doesn't register as parameter changes. After EWMA filtering,
8 * the stable value only updates when the smoothed reading moves past the
9 * deadband threshold.
10 *
11 * ## Pickup mode
12 *
13 * For multi-mode control mappings where the same knob controls different
14 * parameters depending on held buttons, call set_pickup(target) on mode
15 * switch. The output freezes at `target` until the physical knob crosses
16 * that value, then resumes normal tracking. This prevents parameter jumps.
17 *
18 * Usage:
19 * sbl::components::control::Pot knob;
20 * knob.update(raw_adc_value);
21 * int32_t freq = knob.scaled(20, 20000);
22 * if (knob.changed()) { ... value moved past deadband ... }
23 *
24 * // On mode switch:
25 * knob.set_pickup(saved_position); // freeze until knob catches up
26 * if (!knob.picked_up()) { ... show LED feedback ... }
27 */
28
29#ifndef SBL_COMPONENTS_CONTROL_POT_HPP_
30#define SBL_COMPONENTS_CONTROL_POT_HPP_
31
32#include <cstdint>
34#include <sbl/dsp/warp.hpp>
35
36namespace sbl {
37namespace components {
38namespace control {
39
44
45/**
46 * @brief Potentiometer with EWMA smoothing, deadband, and change detection
47 *
48 * @note All public methods are ISR-safe — pure state machine with no hardware I/O.
49 */
50class Pot {
51public:
52 explicit Pot(const PotConfig& config = {})
53 : input_(config.smoothing), deadband_(config.deadband),
54 stable_value_(0), changed_(false) {}
55
56 /**
57 * @brief Feed a new raw ADC sample through EWMA → deadband check
58 *
59 * The EWMA filter smooths the raw value first. Then, if the smoothed
60 * value has moved more than `deadband` from the last stable value,
61 * we update the stable value and set the changed flag.
62 *
63 * When pickup is armed, the EWMA filter tracks the physical position
64 * but stable_value_ stays frozen until the knob crosses the target.
65 */
66 void update(uint16_t raw) {
67 input_.update(raw);
68 uint16_t smoothed = input_.value();
69
70 if (pickup_armed_) {
71 bool now_above = smoothed > pickup_target_;
72 bool crossed = (pickup_above_ && !now_above) ||
73 (!pickup_above_ && now_above);
74
75 // Also pick up if we're within the deadband of the target
76 int32_t dist = static_cast<int32_t>(smoothed) -
77 static_cast<int32_t>(pickup_target_);
78 if (dist < 0) dist = -dist;
79 if (crossed || static_cast<uint16_t>(dist) <= deadband_) {
80 pickup_armed_ = false;
81 stable_value_ = smoothed;
82 changed_ = true;
83 }
84 return;
85 }
86
87 int32_t delta = static_cast<int32_t>(smoothed) - static_cast<int32_t>(stable_value_);
88 if (delta < 0) delta = -delta;
89
90 if (static_cast<uint16_t>(delta) >= deadband_) {
91 stable_value_ = smoothed;
92 changed_ = true;
93 }
94 }
95
96 /** @brief Current stable (deadbanded) value (0-65535) */
97 uint16_t value() const { return stable_value_; }
98
99 /** @brief Last raw (unfiltered) ADC value */
100 uint16_t raw() const { return input_.raw(); }
101
102 /**
103 * @brief Map stable value to an output range (linear)
104 *
105 * Linear interpolation from [0, 65535] -> [min, max].
106 * Uses int64_t intermediate to avoid overflow.
107 * For non-linear mapping (frequency, filter cutoff), see scaled_curved().
108 */
109 int32_t scaled(int32_t min, int32_t max) const {
110 int64_t v = stable_value_;
111 return static_cast<int32_t>(min + (v * (max - min)) / 65535);
112 }
113
114 /**
115 * @brief Map stable value with curve shaping
116 *
117 * Applies ExpCurveWarp::warp() before scaling, useful for knob-friendly
118 * non-linear mapping (frequency, filter cutoff, gain).
119 *
120 * @param min Output minimum (inclusive)
121 * @param max Output maximum (inclusive)
122 * @param curve Shape preset or arbitrary value (see sbl::dsp::curve)
123 * @return Scaled value in [min, max]
124 */
125 int32_t scaled_curved(int32_t min, int32_t max,
126 uint16_t curve = sbl::dsp::curve::Log) const {
127 uint16_t warped = sbl::dsp::ExpCurveWarp::warp(stable_value_, curve);
128 int64_t v = warped;
129 return static_cast<int32_t>(min + (v * (max - min)) / 65535);
130 }
131
132 /**
133 * @brief True if value moved past deadband since last check
134 *
135 * Resets the flag on read — call once per update cycle.
136 */
137 bool changed() {
138 bool c = changed_;
139 changed_ = false;
140 return c;
141 }
142
143 /**
144 * @brief Arm pickup mode — freeze output until knob crosses target
145 *
146 * The physical knob must cross `target` (a raw 0-65535 position)
147 * before stable_value_ resumes tracking. Until then, value(),
148 * scaled(), and scaled_curved() all return values based on `target`.
149 *
150 * Use on mode switch: save the current knob position per-mode,
151 * then set_pickup(saved_position) when re-entering that mode.
152 *
153 * @param target The knob position (0-65535) that must be crossed
154 */
155 void set_pickup(uint16_t target) {
156 uint16_t smoothed = input_.value();
157 int32_t dist = static_cast<int32_t>(smoothed) -
158 static_cast<int32_t>(target);
159 if (dist < 0) dist = -dist;
160
161 // Already at target — no need to arm
162 if (static_cast<uint16_t>(dist) <= deadband_) {
163 stable_value_ = smoothed;
164 return;
165 }
166
167 pickup_target_ = target;
168 pickup_above_ = smoothed > target;
169 pickup_armed_ = true;
170 stable_value_ = target; // freeze output at target
171 }
172
173 /** @brief True if knob has caught up (or pickup was never armed) */
174 bool picked_up() const { return !pickup_armed_; }
175
176 /** @brief Cancel pickup, resume normal tracking immediately */
177 void clear_pickup() { pickup_armed_ = false; }
178
179 /** @brief Reset all state (filter, stable value, changed flag, pickup) */
180 void reset() {
181 input_.reset();
182 stable_value_ = 0;
183 changed_ = false;
184 pickup_armed_ = false;
185 }
186
187private:
188 cv::CvInput input_;
189 uint16_t deadband_;
190 uint16_t stable_value_;
191 uint16_t pickup_target_ = 0;
192 bool changed_;
193 bool pickup_armed_ = false;
194 bool pickup_above_ = false;
195};
196
197} // namespace control
198} // namespace components
199} // namespace sbl
200
201#endif // SBL_COMPONENTS_CONTROL_POT_HPP_
Potentiometer with EWMA smoothing, deadband, and change detection.
Definition pot.hpp:50
bool picked_up() const
True if knob has caught up (or pickup was never armed)
Definition pot.hpp:174
Pot(const PotConfig &config={})
Definition pot.hpp:52
void clear_pickup()
Cancel pickup, resume normal tracking immediately.
Definition pot.hpp:177
uint16_t value() const
Current stable (deadbanded) value (0-65535)
Definition pot.hpp:97
int32_t scaled_curved(int32_t min, int32_t max, uint16_t curve=sbl::dsp::curve::Log) const
Map stable value with curve shaping.
Definition pot.hpp:125
uint16_t raw() const
Last raw (unfiltered) ADC value.
Definition pot.hpp:100
void set_pickup(uint16_t target)
Arm pickup mode — freeze output until knob crosses target.
Definition pot.hpp:155
void update(uint16_t raw)
Feed a new raw ADC sample through EWMA → deadband check.
Definition pot.hpp:66
void reset()
Reset all state (filter, stable value, changed flag, pickup)
Definition pot.hpp:180
int32_t scaled(int32_t min, int32_t max) const
Map stable value to an output range (linear)
Definition pot.hpp:109
bool changed()
True if value moved past deadband since last check.
Definition pot.hpp:137
Smoothed CV input with EWMA filtering and range scaling.
Definition input.hpp:51
uint16_t value() const
Current smoothed value (0–65535)
Definition input.hpp:69
uint16_t raw() const
Last raw (unfiltered) value.
Definition input.hpp:66
void update(uint16_t raw)
Feed a new raw ADC sample.
Definition input.hpp:60
void reset()
Reset filter state.
Definition input.hpp:106
CV Input component — smoothed ADC reading with range scaling.
Smoothing
Smoothing presets for CvInput EWMA filter.
Definition input.hpp:35
@ Medium
alpha=1/8, settles in ~24 samples (pots, knobs)
static constexpr uint16_t Log
Definition warp.hpp:38
Root namespace for all Sound Byte Libs functionality.
Definition aliases.hpp:24
static uint16_t warp(uint16_t phase, uint16_t curve)
Definition warp.hpp:90