Sound Byte Libs 0.5.1-121-g3358a44
C++ firmware library for audio applications on 32-bit ARM Cortex-M processors
Loading...
Searching...
No Matches
merge.hpp
Go to the documentation of this file.
1// sbl/control/merge.hpp — Merge primitives: one parameter, several sources (FDP-074)
2//
3// The merge step runs once per control tick (Stage::Merge) and is the only
4// writer of each bridged level. These primitives decide that level when more
5// than one source can drive a parameter. Plain summation (pitch = knob +
6// V/Oct + MIDI) needs no type.
7//
8// Priority<N> the highest-priority active source wins
9// Latest<N> the last source to move wins
10// Pickup a takeover source must reach the held value before it captures it
11//
12// All work on su floats, hold no pointers, allocate nothing, and run in one
13// context (the control tick).
14//
15// Usage:
16// sbl::control::Pickup fold; // knob vs. MIDI CC
17// // Stage::Drain, on the CC: fold.hold(cc_su);
18// // Stage::Merge, every tick: s_fold_su = fold.process(knob.value());
19// // UI slot: led = fold.caught();
20
21#ifndef SBL_CONTROL_MERGE_HPP_
22#define SBL_CONTROL_MERGE_HPP_
23
24#include <cstdint>
25
26namespace sbl::control {
27
28// ─── Priority ────────────────────────────────────────────────────────
29
30/**
31 * @brief The highest-priority active source wins
32 *
33 * Source 0 is the highest priority. Each source reports a value and whether
34 * it is active (a MIDI source while its gate is high; a knob always). With
35 * no source active, the last resolved value holds.
36 */
37template<uint8_t N>
38class Priority {
39 static_assert(N > 0, "Priority needs at least one source");
40
41public:
42 void set(uint8_t source, float value, bool active) {
43 if (source >= N) return;
44 values_[source] = value;
45 active_[source] = active;
46 resolve();
47 }
48
49 float value() const { return value_; }
50
51 /// Winning source index, or -1 if none is active (value() is holding).
52 int8_t winner() const { return winner_; }
53
54private:
55 void resolve() {
56 winner_ = -1;
57 for (uint8_t i = 0; i < N; ++i) {
58 if (active_[i]) {
59 winner_ = static_cast<int8_t>(i);
60 value_ = values_[i];
61 return;
62 }
63 }
64 }
65
66 float values_[N]{};
67 bool active_[N]{};
68 float value_ = 0.0f;
69 int8_t winner_ = -1;
70};
71
72// ─── Latest ──────────────────────────────────────────────────────────
73
74/**
75 * @brief The last source to move wins
76 *
77 * Continuous sources (a pot polled every tick) report through update(): a
78 * report counts as a write only when the source has moved more than the
79 * threshold since its own last write, so a resting knob's noise cannot take
80 * the parameter from a CC. The first report from a source sets its baseline
81 * without taking ownership. Event sources (a CC) report through write():
82 * every call is a write.
83 */
84template<uint8_t N>
85class Latest {
86 static_assert(N > 0, "Latest needs at least one source");
87
88public:
89 explicit Latest(float threshold = 0.001f) : threshold_(threshold) {}
90
91 void update(uint8_t source, float value) {
92 if (source >= N) return;
93 if (!seen_[source]) {
94 seen_[source] = true;
95 last_[source] = value;
96 return;
97 }
98 float moved = value - last_[source];
99 if (moved < 0.0f) moved = -moved;
100 if (moved > threshold_) take(source, value);
101 }
102
103 void write(uint8_t source, float value) {
104 if (source >= N) return;
105 seen_[source] = true;
106 take(source, value);
107 }
108
109 float value() const { return value_; }
110
111 /// Source that wrote last, or -1 before any write.
112 int8_t owner() const { return owner_; }
113
114private:
115 void take(uint8_t source, float value) {
116 last_[source] = value;
117 value_ = value;
118 owner_ = static_cast<int8_t>(source);
119 }
120
121 float last_[N]{};
122 bool seen_[N]{};
123 float threshold_;
124 float value_ = 0.0f;
125 int8_t owner_ = -1;
126};
127
128// ─── Pickup ──────────────────────────────────────────────────────────
129
130/**
131 * @brief A takeover source must reach the held value before it captures it
132 *
133 * When another source sets the parameter (hold()), the takeover source — a
134 * knob — is ignored until it crosses the held value or comes within the
135 * window of it. From then on the parameter follows the knob until the next
136 * hold(). Without a hold the knob drives the parameter directly.
137 */
138class Pickup {
139public:
140 explicit Pickup(float window = 0.01f) : window_(window) {}
141
142 /// Another source set the parameter; the takeover source must now reach it.
143 void hold(float value) {
144 value_ = value;
145 armed_ = true;
146 side_known_ = false;
147 }
148
149 /// Feed the takeover source (every tick). Returns the parameter value.
150 float process(float source) {
151 if (armed_) {
152 const float d = source - value_;
153 const bool above = d > 0.0f;
154 const bool within = (d <= window_) && (d >= -window_);
155 const bool crossed = side_known_ && (above != above_);
156 if (!within && !crossed) {
157 above_ = above;
158 side_known_ = true;
159 return value_;
160 }
161 armed_ = false;
162 }
163 value_ = source;
164 return value_;
165 }
166
167 /// True while the takeover source drives the parameter.
168 bool caught() const { return !armed_; }
169
170 float value() const { return value_; }
171
172 /// Hand the parameter to the takeover source now (e.g. a panel reset).
173 void release() { armed_ = false; }
174
175private:
176 float window_;
177 float value_ = 0.0f;
178 bool armed_ = false;
179 bool above_ = false;
180 bool side_known_ = false;
181};
182
183} // namespace sbl::control
184
185#endif // SBL_CONTROL_MERGE_HPP_
The last source to move wins.
Definition merge.hpp:85
void write(uint8_t source, float value)
Definition merge.hpp:103
void update(uint8_t source, float value)
Definition merge.hpp:91
Latest(float threshold=0.001f)
Definition merge.hpp:89
float value() const
Definition merge.hpp:109
int8_t owner() const
Source that wrote last, or -1 before any write.
Definition merge.hpp:112
A takeover source must reach the held value before it captures it.
Definition merge.hpp:138
float value() const
Definition merge.hpp:170
Pickup(float window=0.01f)
Definition merge.hpp:140
float process(float source)
Feed the takeover source (every tick). Returns the parameter value.
Definition merge.hpp:150
void release()
Hand the parameter to the takeover source now (e.g. a panel reset).
Definition merge.hpp:173
bool caught() const
True while the takeover source drives the parameter.
Definition merge.hpp:168
void hold(float value)
Another source set the parameter; the takeover source must now reach it.
Definition merge.hpp:143
The highest-priority active source wins.
Definition merge.hpp:38
float value() const
Definition merge.hpp:49
void set(uint8_t source, float value, bool active)
Definition merge.hpp:42
int8_t winner() const
Winning source index, or -1 if none is active (value() is holding).
Definition merge.hpp:52
Control Stack v2: scheduler, event queue, merge, CC mapping, calibration, presets.