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
scheduler.hpp
Go to the documentation of this file.
1// sbl/control/scheduler.hpp — The control context: one base tick, divider slots,
2// fixed intra-tick order (FDP-073)
3//
4// The app drives run() from a single base tick — the 1 kHz timer ISR on
5// hardware, the timer thread on the workbench — and passes the audio sample
6// clock. Each slot runs every Nth base tick. Subscribers are told how many
7// samples elapsed since their slot last ran, derived from the clock, so an
8// irregular base tick never skews time-constant math (the rate invariant:
9// components are told elapsed time, never assume a call rate).
10//
11// Every base tick runs in stage order, so the signal-transport update order
12// is enforced by construction:
13//
14// Drain event queues → converters (MIDI parsed elsewhere arrives here)
15// Update sources and components: tick(elapsed_samples)
16// Merge per-parameter merge policies resolve (FDP-074)
17// Publish bridge writes — the only state the audio context reads
18//
19// Within a stage, due subscribers run in subscription order. Apps subscribe;
20// components never self-register. Static storage, no allocation.
21//
22// Usage:
23// enum : uint8_t { k1kHz, k200Hz, k60Hz };
24// sbl::control::Scheduler<3> sched{{1, 5, 17}};
25// sched.subscribe<MyWidget, &MyWidget::tick>(Stage::Update, k200Hz, widget);
26// sched.subscribe(Stage::Drain, k1kHz, drain_midi, nullptr);
27//
28// // base tick (1 kHz timer ISR / timer thread)
29// sched.run(sbl::hal::audio::SampleClock::now());
30//
31// Contexts: run() from exactly one context. subscribe() at init, before the
32// first run().
33
34#ifndef SBL_CONTROL_SCHEDULER_HPP_
35#define SBL_CONTROL_SCHEDULER_HPP_
36
37#include <cstdint>
38
39namespace sbl::control {
40
41/// Intra-tick order. Every due subscriber of a stage runs before any of the next.
42enum class Stage : uint8_t {
43 Drain,
44 Update,
45 Merge,
46 Publish,
47};
48
49inline constexpr uint8_t kStageCount = 4;
50
51/// Subscriber callback: context pointer and samples elapsed since its slot last ran.
52using TickFn = void (*)(void* context, uint32_t elapsed_samples);
53
54template<uint8_t Slots, uint8_t Capacity = 16>
55class Scheduler {
56 static_assert(Slots > 0, "Scheduler needs at least one slot");
57 static_assert(Capacity > 0, "Scheduler needs subscriber capacity");
58
59public:
60 /// @param dividers Base ticks per run, per slot (0 is treated as 1).
61 explicit Scheduler(const uint16_t (&dividers)[Slots]) {
62 for (uint8_t s = 0; s < Slots; ++s) {
63 divider_[s] = dividers[s] ? dividers[s] : 1;
64 }
65 }
66
67 /// Add a subscriber. Returns false if full, the slot is out of range, or fn is null.
68 bool subscribe(Stage stage, uint8_t slot, TickFn fn, void* context) {
69 if (count_ >= Capacity || slot >= Slots || fn == nullptr) return false;
70 subs_[count_++] = {fn, context, stage, slot};
71 return true;
72 }
73
74 /// Subscribe a member `void T::method(uint32_t elapsed_samples)`.
75 template<typename T, void (T::*Method)(uint32_t)>
76 bool subscribe(Stage stage, uint8_t slot, T& object) {
77 return subscribe(stage, slot,
78 [](void* c, uint32_t elapsed) { (static_cast<T*>(c)->*Method)(elapsed); },
79 &object);
80 }
81
82 /// One base tick. `sample_clock` is the audio frame count (wrapping).
83 /// The first call establishes the baseline: every slot runs, told 0.
85 if (!started_) {
86 for (uint8_t s = 0; s < Slots; ++s) last_[s] = sample_clock;
87 started_ = true;
88 }
89
90 bool due[Slots];
92 for (uint8_t s = 0; s < Slots; ++s) {
93 due[s] = (phase_[s] == 0);
94 elapsed[s] = 0;
95 if (due[s]) {
96 elapsed[s] = sample_clock - last_[s];
97 last_[s] = sample_clock;
98 }
99 if (++phase_[s] >= divider_[s]) phase_[s] = 0;
100 }
101
102 for (uint8_t st = 0; st < kStageCount; ++st) {
103 const Stage stage = static_cast<Stage>(st);
104 for (uint8_t i = 0; i < count_; ++i) {
105 const Subscriber& sub = subs_[i];
106 if (sub.stage == stage && due[sub.slot]) {
107 sub.fn(sub.context, elapsed[sub.slot]);
108 }
109 }
110 }
111 }
112
113 uint8_t subscriber_count() const { return count_; }
114 uint16_t divider(uint8_t slot) const { return slot < Slots ? divider_[slot] : 0; }
115 static constexpr uint8_t slots() { return Slots; }
116 static constexpr uint8_t capacity() { return Capacity; }
117
118private:
119 struct Subscriber {
120 TickFn fn = nullptr;
121 void* context = nullptr;
122 Stage stage = Stage::Update;
123 uint8_t slot = 0;
124 };
125
126 Subscriber subs_[Capacity]{};
127 uint16_t divider_[Slots]{};
128 uint16_t phase_[Slots]{};
129 uint32_t last_[Slots]{};
130 uint8_t count_ = 0;
131 bool started_ = false;
132};
133
134} // namespace sbl::control
135
136#endif // SBL_CONTROL_SCHEDULER_HPP_
void run(uint32_t sample_clock)
Definition scheduler.hpp:84
static constexpr uint8_t capacity()
static constexpr uint8_t slots()
bool subscribe(Stage stage, uint8_t slot, T &object)
Subscribe a member void T::method(uint32_t elapsed_samples).
Definition scheduler.hpp:76
Scheduler(const uint16_t(&dividers)[Slots])
Definition scheduler.hpp:61
uint16_t divider(uint8_t slot) const
bool subscribe(Stage stage, uint8_t slot, TickFn fn, void *context)
Add a subscriber. Returns false if full, the slot is out of range, or fn is null.
Definition scheduler.hpp:68
uint8_t subscriber_count() const
Control Stack v2: scheduler, event queue, merge, CC mapping, calibration, presets.
constexpr uint8_t kStageCount
Definition scheduler.hpp:49
Stage
Intra-tick order. Every due subscriber of a stage runs before any of the next.
Definition scheduler.hpp:42