Loading...
Searching...
No Matches
RateLimiter_v2.hpp
1#pragma once
2#include <Fx/NoteValueGrid.hpp>
3#include <Fx/Types.hpp>
4
5#include <ossia/dataflow/exec_state_facade.hpp>
6#include <ossia/dataflow/token_request.hpp>
7#include <ossia/dataflow/value_port.hpp>
8
9#include <halp/callback.hpp>
10#include <halp/controls.hpp>
11#include <halp/layout.hpp>
12#include <halp/meta.hpp>
13#include <halp/midi.hpp>
14
15#include <cmath>
16
17#include <algorithm>
18#include <optional>
19
20namespace Nodes::RateLimiter::v2
21{
22struct Node
23{
24 halp_meta(name, "Rate Limiter")
25 halp_meta(c_name, "RateLimiter")
26 halp_meta(category, "Control/Mappings")
27 halp_meta(
28 manual_url, "https://ossia.io/score-docs/processes/rate-limiter.html#rate-limiter")
29 halp_meta(author, "ossia score")
30 halp_meta(
31 description,
32 "Limit a value stream to one value per interval, on the musical grid when "
33 "the interval is synced, or debounce after a quiet interval")
34 halp_meta(uuid, "485d39ed-e495-448c-95df-5f2ea905ef5b")
35 halp_flag(process_exec);
36
37 enum class Mode
38 {
39 Limit,
40 Debounce
41 };
42
43 struct ins
44 {
45 ossia_port<"in", ossia::value_port> port{};
46 struct : halp::time_chooser<
47 "Interval", halp::range{0., 10., 0.01}, halp::bar_sync::restart>
48 {
49 halp_meta(
50 description,
51 "Minimum interval between two values (synced: at most one per note "
52 "value, on the grid), or quiet time before a debounced value")
53 } interval;
54 struct : halp::enum_t<Mode, "Mode">
55 {
56 void update(Node& self)
57 {
58 if(self.previous_mode != this->value)
59 self.reset();
60 }
61 } mode;
62 struct : halp::toggle<"Send latest", halp::default_on_toggle>
63 {
64 halp_meta(
65 description,
66 "When limiting, hold the latest value that comes too early and send "
67 "it when the interval is over (synced: on the next grid point); off, "
68 "drop it (synced: the first value of each grid step goes through)")
69 } latest;
70 } inputs;
71 struct
72 {
73 halp::timed_callback<"out", ossia::value> out;
74 } outputs;
75
76 // Raw value ports carry buffer-relative timestamps, whereas timed callbacks
77 // take tick-relative timestamps. The native token retains the slice offset
78 // that tick_flicks cannot represent.
79 using tick = ossia::token_request;
80 ossia::exec_state_facade ossia_state;
81
82 static constexpr double flicks_per_s = 705'600'000.;
84 std::optional<long double> last_sent;
85 std::optional<ossia::value> pending;
86 long double deadline{};
87 std::optional<int64_t> previous_end;
88 Mode previous_mode{Mode::Limit};
91 ossia::small_vector<const ossia::timed_value*, 16> ordered_events;
92
93 void reset() noexcept
94 {
95 pending.reset();
96 previous_end.reset();
97 last_sent.reset();
98 sent_in_step = false;
99 previous_mode = inputs.mode.value;
100 }
101
102 void prepare(halp::setup) noexcept { reset(); }
103 void start() noexcept { reset(); }
104 void stop() noexcept { reset(); }
105 void transport(auto) noexcept { reset(); }
106
108 template <typename F>
109 void for_each_event(int64_t start, int64_t frames, F&& f)
110 {
111 const auto& data = inputs.port.value->get_data();
112 const auto in_tick = [&](const ossia::timed_value& v) {
113 return v.timestamp >= start && v.timestamp < start + frames;
114 };
115 if(std::is_sorted(data.begin(), data.end(), [](const auto& a, const auto& b) {
116 return a.timestamp < b.timestamp;
117 }))
118 {
119 for(const auto& v : data)
120 if(in_tick(v))
121 f(v);
122 }
123 else
124 {
125 // Fan-in may append independently sorted streams. Sort pointers, not
126 // arbitrary values, retaining arrival order for equal timestamps.
127 ordered_events.clear();
128 for(const auto& v : data)
129 if(in_tick(v))
130 ordered_events.push_back(&v);
131 std::sort(
132 ordered_events.begin(), ordered_events.end(),
133 [](const auto* a, const auto* b) {
134 return a->timestamp < b->timestamp || (a->timestamp == b->timestamp && a < b);
135 });
136 for(const auto* v : ordered_events)
137 f(*v);
138 }
139 }
140
141 void operator()(const tick& t)
142 {
143 if(previous_mode != inputs.mode.value
144 || (previous_end && t.prev_date.impl != *previous_end))
145 reset();
146 previous_end = t.date.impl;
147
148 const auto [start, frames] = ossia_state.timings(t);
149 if(inputs.mode.value == Mode::Debounce)
150 {
151 // A rewind or stopped transport cannot carry a forward-time deadline.
152 if(t.date <= t.prev_date || t.speed <= 0.)
153 {
154 pending.reset();
155 return;
156 }
157 if(frames > 0)
158 debounce(t, start, frames);
159 }
160 else if(frames <= 0)
161 {
162 return;
163 }
164 else if(!inputs.latest.value)
165 {
166 pending.reset();
167 if(inputs.interval.sync)
168 drop_on_grid(t, start, frames);
169 else
170 limit(t, start, frames);
171 }
172 else if(inputs.interval.sync)
173 {
174 sent_in_step = false;
175 limit_on_grid(t, start, frames);
176 }
177 else
178 {
179 limit(t, start, frames);
180 }
181 }
182
187 void limit(const tick& t, int64_t start, int64_t frames)
188 {
189 const long double begin = t.prev_date.impl;
190 const long double duration = static_cast<long double>(t.date.impl) - begin;
191 const long double delay = std::max(0.f, inputs.interval.value) * flicks_per_s;
192 // The interval, a float, may land a hair past the sample it means.
193 const long double slack = duration > 0. ? 1e-3L * duration / frames : 0.L;
194
195 // The held value, if its time has come by `date`
196 const auto flush = [&](long double date) {
197 if(!pending || !last_sent || duration <= 0.)
198 return;
199 const long double due = *last_sent + delay;
200 if(due > date)
201 return;
202 // The first sample at or after the date
203 const auto frame
204 = std::max(0.L, std::ceil((due - slack - begin) * frames / duration));
205 if(frame >= frames)
206 return;
207 outputs.out(static_cast<int64_t>(frame), std::move(*pending));
208 pending.reset();
209 last_sent = std::max(due, begin);
210 };
211
212 for_each_event(start, frames, [&](const ossia::timed_value& v) {
213 const long double date
214 = duration > 0. ? begin + (v.timestamp - start) * duration / frames : begin;
215 flush(date);
216 if(!last_sent || date + slack >= *last_sent + delay)
217 {
218 pending.reset();
219 last_sent = date;
220 outputs.out(v.timestamp - start, v.value);
221 }
222 else if(inputs.latest.value)
223 {
224 pending = v.value;
225 }
226 });
227 flush(t.date.impl);
228 }
229
231 ossia::quantification_points grid(const tick& t) const noexcept
232 {
233 if(!t.forward())
234 return {};
235 const double rate
236 = grid_rate(std::max(0.f, inputs.interval.value), t.tempo, inputs.interval.sync);
237 if(inputs.interval.bar_sync || !(rate > 1.))
238 return t.get_quantification_dates(rate);
239
240 // One pulse across the bar lines
241 ossia::quantification_points res;
242 const double start = t.musical_start_position;
243 const double end = t.musical_end_position;
244 const bool valid_sig = t.signature.upper > 0 && t.signature.lower > 0;
245 const double quarters_in_bar
246 = valid_sig ? 4. * t.signature.upper / t.signature.lower : 4.;
247 const double scale = double((t.date - t.prev_date).impl) / (end - start);
248 for_each_pulse_point(
249 start, end, t.musical_start_last_signature, 4. / rate, quarters_in_bar,
250 [&](double p, int64_t index) {
251 res.push_back(
252 {t.prev_date + ossia::time_value{int64_t((p - start) * scale)}, index, p});
253 });
254 return res;
255 }
256
258 void limit_on_grid(const tick& t, int64_t start, int64_t frames)
259 {
260 const auto points = grid(t);
261 auto point = points.begin();
262 const auto emit_until = [&](int64_t frame) {
263 for(; point != points.end(); ++point)
264 {
265 const int64_t f
266 = t.physical_position(point->position, ossia_state.modelToSamples());
267 if(f > frame)
268 break;
269 if(pending)
270 {
271 outputs.out(f, std::move(*pending));
272 pending.reset();
273 }
274 }
275 };
276 for_each_event(start, frames, [&](const ossia::timed_value& v) {
277 // The grid points before this value send what came before it
278 emit_until(v.timestamp - start - 1);
279 pending = v.value;
280 });
281 emit_until(frames);
282 }
283
287 void drop_on_grid(const tick& t, int64_t start, int64_t frames)
288 {
289 const auto points = grid(t);
290 auto point = points.begin();
291 for_each_event(start, frames, [&](const ossia::timed_value& v) {
292 const int64_t frame = v.timestamp - start;
293 for(; point != points.end()
294 && t.physical_position(point->position, ossia_state.modelToSamples())
295 <= frame;
296 ++point)
297 sent_in_step = false;
298 if(!sent_in_step)
299 {
300 sent_in_step = true;
301 outputs.out(frame, v.value);
302 }
303 });
304 if(point != points.end())
305 sent_in_step = false;
306 }
307
308 void debounce(const tick& t, int64_t start, int64_t frames)
309 {
310 // The quiet interval is model time (a synced one at the current tempo),
311 // not the musical grid. Map through the carried sample span, including fractional
312 // flicks, so non-unit transport speeds and partial buffers stay accurate.
313 const long double begin = t.prev_date.impl;
314 const long double duration = static_cast<long double>(t.date.impl) - begin;
315 const int64_t delay = int64_t(std::max(0.f, inputs.interval.value) * flicks_per_s);
316 const auto emit = [&] {
317 // Never emit before the quiet interval has elapsed: round up to the first
318 // eligible sample. The tick is half-open; its end belongs to the next.
319 const auto frame
320 = std::max(0.L, std::ceil((deadline - begin) * frames / duration));
321 if(frame < frames)
322 {
323 outputs.out(static_cast<int64_t>(frame), std::move(*pending));
324 pending.reset();
325 }
326 };
327 for_each_event(start, frames, [&](const ossia::timed_value& v) {
328 const auto date = begin + (v.timestamp - start) * duration / frames;
329 // An arrival exactly at the deadline wins, extending the same burst.
330 // Zero delay is immediate for every event, including equal timestamps.
331 if(pending && deadline < date)
332 emit();
333 if(delay == 0)
334 {
335 pending.reset();
336 outputs.out(v.timestamp - start, v.value);
337 }
338 else
339 {
340 pending = v.value;
341 deadline = date + delay;
342 }
343 });
344 if(pending && deadline < t.date.impl)
345 emit();
346 }
347
348 struct ui
349 {
350 halp_meta(layout, halp::layouts::hbox)
351 halp_meta(background, halp::colors::background_mid)
352 halp::control<&ins::interval> t;
353 halp::control<&ins::mode> mode;
354 halp::control<&ins::latest> latest;
355 };
356};
357}
Definition RateLimiter_v2.hpp:44
Definition RateLimiter_v2.hpp:349
Definition RateLimiter_v2.hpp:23
void limit(const tick &t, int64_t start, int64_t frames)
Definition RateLimiter_v2.hpp:187
bool sent_in_step
Synced, not sending the latest: a value went out since the last grid point.
Definition RateLimiter_v2.hpp:90
void drop_on_grid(const tick &t, int64_t start, int64_t frames)
Definition RateLimiter_v2.hpp:287
std::optional< long double > last_sent
Free limit: when the last value went out.
Definition RateLimiter_v2.hpp:84
ossia::quantification_points grid(const tick &t) const noexcept
Synced, the interval is a note value: the points of that grid in the tick.
Definition RateLimiter_v2.hpp:231
void for_each_event(int64_t start, int64_t frames, F &&f)
The input values of the tick, in time order.
Definition RateLimiter_v2.hpp:109
void limit_on_grid(const tick &t, int64_t start, int64_t frames)
The latest value goes out on each grid point.
Definition RateLimiter_v2.hpp:258
Definition Types.hpp:40