Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
param_cursor.hpp
Go to the documentation of this file.
1#pragma once
2
3// Block-local parameter cursor for sample-accurate automation.
4//
5// The cursor advances over a sorted ParameterEventQueue and maintains a
6// fixed-size value view for the parameters touched by the block. It never
7// mutates StateStore, allocates heap storage, or invokes listeners.
8
10#include <pulp/state/store.hpp>
11#include <algorithm>
12#include <array>
13#include <cmath>
14#include <cstddef>
15#include <cstdint>
16#include <limits>
17#include <span>
18
19namespace pulp::state {
20
23 float value = 0.0f;
24};
25
27public:
28 ParamCursor(const StateStore& store,
30 std::span<const ParamSnapshotEntry> initial_values = {})
31 : store_(&store)
32 , events_(events ? events->events() : std::span<const ParameterEvent>{})
33 {
34 for (const auto& initial : initial_values) {
35 add_or_update(initial.param_id, initial.value, true);
36 }
37
38 for (const auto& event : events_) {
39 ensure_event_entry(event.param_id);
40 }
41 }
42
43 void advance_to(int32_t sample_offset) {
44 if (sample_offset < position_) return;
45
46 while (next_event_ < events_.size() &&
47 events_[next_event_].sample_offset <= sample_offset) {
48 update_ramps_to(events_[next_event_].sample_offset);
49 apply_event(events_[next_event_]);
50 ++next_event_;
51 }
52
53 update_ramps_to(sample_offset);
54 position_ = sample_offset;
55 }
56
57 float value(ParamID id) const {
58 if (const auto* entry = find_entry(id)) return entry->value;
59 return store_ ? store_->get_value(id) : 0.0f;
60 }
61
62 float value_at(ParamID id, int32_t sample_offset) const {
63 if (const auto* entry = find_entry(id)) {
64 return value_for_entry_at(*entry, sample_offset);
65 }
66 return store_ ? store_->get_value(id) : 0.0f;
67 }
68
69 bool is_ramping(ParamID id) const {
70 if (const auto* entry = find_entry(id)) return entry->ramping;
71 return false;
72 }
73
74 // In-flight ramp introspection, for continuing a ramp across block
75 // boundaries (a caller that owns the cursor's lifetime can read the active
76 // ramp's target + block-relative end offset and re-seed the next block).
77 // Meaningful only while is_ramping(id) is true; both return a benign
78 // fallback for an untracked or non-ramping param.
79 float ramp_target(ParamID id) const {
80 if (const auto* entry = find_entry(id)) return entry->ramp_target_value;
81 return value(id);
82 }
83 int32_t ramp_end_offset(ParamID id) const {
84 if (const auto* entry = find_entry(id)) return entry->ramp_end_sample_offset;
85 return 0;
86 }
87
88 bool is_tracked(ParamID id) const {
89 return find_entry(id) != nullptr;
90 }
91
92 std::size_t tracked_param_count() const { return entry_count_; }
93 int32_t position() const { return position_; }
94
95private:
96 struct Entry {
97 ParamID param_id = 0;
98 float value = 0.0f;
99 float ramp_start_value = 0.0f;
100 float ramp_target_value = 0.0f;
101 int32_t ramp_start_sample_offset = 0;
102 int32_t ramp_end_sample_offset = 0;
103 bool ramping = false;
104 };
105
106 Entry* find_entry(ParamID id) {
107 for (std::size_t i = 0; i < entry_count_; ++i) {
108 if (entries_[i].param_id == id) return &entries_[i];
109 }
110 return nullptr;
111 }
112
113 const Entry* find_entry(ParamID id) const {
114 for (std::size_t i = 0; i < entry_count_; ++i) {
115 if (entries_[i].param_id == id) return &entries_[i];
116 }
117 return nullptr;
118 }
119
120 float clamp_value(ParamID id, float value) const {
121 if (store_) {
122 if (const auto* info = store_->info(id)) {
123 // Host/injected automation is an untrusted boundary. Like
124 // StateStore::set_value(), replace NaN/Inf with the effective
125 // declared default before clamping so ramps and downstream
126 // integer/enum decoders never observe a non-finite value.
127 if (!std::isfinite(value)) value = info->range.default_value;
128 return std::clamp(value, info->range.min, info->range.max);
129 }
130 }
131 return value;
132 }
133
134 bool is_registered(ParamID id) const {
135 return store_ && store_->info(id) != nullptr;
136 }
137
138 void add_or_update(ParamID id, float value, bool allow_unregistered) {
139 if (!allow_unregistered && !is_registered(id)) return;
140 if (auto* entry = find_entry(id)) {
141 entry->value = clamp_value(id, value);
142 entry->ramping = false;
143 return;
144 }
145 if (entry_count_ >= entries_.size()) return;
146 const float clamped = clamp_value(id, value);
147 entries_[entry_count_++] = Entry{
148 .param_id = id,
149 .value = clamped,
150 .ramp_start_value = clamped,
151 .ramp_target_value = clamped,
152 };
153 }
154
155 void ensure_event_entry(ParamID id) {
156 if (find_entry(id) != nullptr) return;
157 if (!is_registered(id)) return;
158 add_or_update(id, store_->get_value(id), false);
159 }
160
161 float value_for_entry_at(const Entry& entry, int32_t sample_offset) const {
162 if (!entry.ramping) return entry.value;
163 if (sample_offset <= entry.ramp_start_sample_offset) {
164 return entry.ramp_start_value;
165 }
166 if (sample_offset >= entry.ramp_end_sample_offset) {
167 return entry.ramp_target_value;
168 }
169
170 const auto elapsed = static_cast<float>(sample_offset - entry.ramp_start_sample_offset);
171 const auto duration = static_cast<float>(entry.ramp_end_sample_offset
172 - entry.ramp_start_sample_offset);
173 const float t = duration > 0.0f ? (elapsed / duration) : 1.0f;
174 return entry.ramp_start_value + (entry.ramp_target_value - entry.ramp_start_value) * t;
175 }
176
177 void update_ramps_to(int32_t sample_offset) {
178 for (std::size_t i = 0; i < entry_count_; ++i) {
179 auto& entry = entries_[i];
180 if (!entry.ramping) continue;
181 entry.value = value_for_entry_at(entry, sample_offset);
182 if (sample_offset >= entry.ramp_end_sample_offset) {
183 entry.value = entry.ramp_target_value;
184 entry.ramping = false;
185 }
186 }
187 }
188
189 int32_t ramp_end_for(const ParameterEvent& event) const {
190 if (event.ramp_duration_sample_frames <= 0) return event.sample_offset;
191 const auto max = std::numeric_limits<int32_t>::max();
192 if (event.sample_offset > max - event.ramp_duration_sample_frames) return max;
193 return event.sample_offset + event.ramp_duration_sample_frames;
194 }
195
196 void apply_event(const ParameterEvent& event) {
197 if (!is_registered(event.param_id)) return;
198 ensure_event_entry(event.param_id);
199 auto* entry = find_entry(event.param_id);
200 if (!entry) return;
201
202 const float target = clamp_value(event.param_id, event.value);
203 if (event.ramp_duration_sample_frames <= 0) {
204 entry->value = target;
205 entry->ramping = false;
206 return;
207 }
208
209 entry->ramp_start_value = entry->value;
210 entry->ramp_target_value = target;
211 entry->ramp_start_sample_offset = event.sample_offset;
212 entry->ramp_end_sample_offset = ramp_end_for(event);
213 entry->ramping = entry->ramp_end_sample_offset > entry->ramp_start_sample_offset;
214 if (!entry->ramping) {
215 entry->value = target;
216 }
217 }
218
219 const StateStore* store_ = nullptr;
220 std::span<const ParameterEvent> events_;
221 std::size_t next_event_ = 0;
222 std::array<Entry, ParameterEventQueue::kCapacity> entries_{};
223 std::size_t entry_count_ = 0;
224 int32_t position_ = std::numeric_limits<int32_t>::min();
225};
226
227} // namespace pulp::state
float value_at(ParamID id, int32_t sample_offset) const
Definition param_cursor.hpp:62
bool is_ramping(ParamID id) const
Definition param_cursor.hpp:69
std::size_t tracked_param_count() const
Definition param_cursor.hpp:92
void advance_to(int32_t sample_offset)
Definition param_cursor.hpp:43
int32_t ramp_end_offset(ParamID id) const
Definition param_cursor.hpp:83
int32_t position() const
Definition param_cursor.hpp:93
bool is_tracked(ParamID id) const
Definition param_cursor.hpp:88
ParamCursor(const StateStore &store, const ParameterEventQueue *events, std::span< const ParamSnapshotEntry > initial_values={})
Definition param_cursor.hpp:28
float ramp_target(ParamID id) const
Definition param_cursor.hpp:79
float value(ParamID id) const
Definition param_cursor.hpp:57
Definition parameter_event_queue.hpp:82
Centralized parameter storage with lock-free value access.
Definition store.hpp:164
Definition child_process_manager.hpp:18
@ info
Definition gpu_diagnostics.hpp:56
@ i
Definition formant_filter_bank.hpp:20
SequencerStateChannel is the audio-safe transport for a plugin's structured NON-parameter state: the ...
Definition audio_device_manager.hpp:48
uint32_t ParamID
Unique parameter identifier, stable across plugin versions.
Definition parameter.hpp:55
@ t
Definition input_events.hpp:41
STL namespace.
Definition param_cursor.hpp:21
ParamID param_id
Definition param_cursor.hpp:22
float value
Definition param_cursor.hpp:23
Definition parameter_event_queue.hpp:34