Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
graph_event_contract.hpp
Go to the documentation of this file.
1#pragma once
2
11
12#include <pulp/midi/buffer.hpp>
15
16#include <algorithm>
17#include <array>
18#include <cstddef>
19#include <cstdint>
20#include <limits>
21#include <span>
22
23namespace pulp::host {
24
25namespace detail {
26struct GraphEventLaneTestAccess;
27} // namespace detail
28
40
49 std::uint64_t epoch = 0;
50 std::int64_t absolute_sample = 0;
51 std::uint64_t producer_sequence = 0;
52 std::uint32_t producer = 0;
55 bool has_ump = false;
56};
57
65template <std::size_t Capacity = 256> class HostEventIngress final {
66 public:
67 static_assert(Capacity > 0);
68
69 HostEventResult begin_block(std::uint64_t epoch, std::uint64_t sample_start,
70 std::size_t frames) noexcept {
71 if (active_)
73 if (epoch == 0 || frames == 0 || frames > kMaxFrames ||
74 sample_start > std::numeric_limits<std::uint64_t>::max() - frames)
76 // Staged records belong to one activation epoch. A new epoch starts
77 // with an empty staging area so a future record from the retired
78 // epoch cannot be projected into the replacement graph.
79 if (epoch != epoch_)
80 size_ = 0;
81 // A forward jump (seek/loop) can leave staged records behind the new
82 // window; they can never be projected and publish() refuses late
83 // records, so evict them rather than leak staging capacity.
84 if (epoch == epoch_) {
85 std::size_t keep = 0;
86 for (std::size_t i = 0; i < size_; ++i) {
87 if (records_[i].epoch == epoch &&
88 static_cast<std::uint64_t>(records_[i].absolute_sample) >= sample_start)
89 records_[keep++] = records_[i];
90 }
91 size_ = keep;
92 }
93 epoch_ = epoch;
94 sample_start_ = sample_start;
95 frames_ = frames;
96 active_ = true;
98 }
99
100 HostEventResult publish(const HostEventRecord& event) noexcept {
101 if (!active_ || event.epoch != epoch_)
103 if (event.producer == 0)
105 if (event.absolute_sample < 0)
107 if (static_cast<std::uint64_t>(event.absolute_sample) < sample_start_)
109 for (std::size_t i = 0; i < size_; ++i) {
110 // Sequence numbers are a producer-local identity/tie key. Host
111 // adapters may publish records out of timestamp order within the
112 // current block, so reject only a duplicate sequence rather than
113 // treating a lower (but unique) sequence as a mutation.
114 if (records_[i].producer == event.producer &&
115 event.producer_sequence == records_[i].producer_sequence)
117 }
118 if (size_ == Capacity)
120 records_[size_++] = event;
122 }
123
127 HostEventResult project(std::span<HostEventRecord> out, std::size_t& count) noexcept {
128 count = 0;
129 if (!active_)
131 const auto end = sample_start_ + frames_;
132 std::size_t matching = 0;
133 for (std::size_t i = 0; i < size_; ++i) {
134 if (records_[i].epoch != epoch_)
135 continue;
136 const auto sample = static_cast<std::uint64_t>(records_[i].absolute_sample);
137 if (sample >= sample_start_ && sample < end)
138 ++matching;
139 }
140 if (matching > out.size())
142 for (std::size_t i = 0; i < size_; ++i) {
143 if (records_[i].epoch != epoch_)
144 continue;
145 const auto sample = static_cast<std::uint64_t>(records_[i].absolute_sample);
146 if (sample >= sample_start_ && sample < end)
147 out[count++] = records_[i];
148 }
149 // Insertion sort is bounded and allocation-free. Producer identity is
150 // the deterministic tie key when two producers use the same sequence.
151 for (std::size_t i = 1; i < count; ++i) {
152 auto value = out[i];
153 std::size_t j = i;
154 while (j > 0 && less(value, out[j - 1])) {
155 out[j] = out[j - 1];
156 --j;
157 }
158 out[j] = value;
159 }
160 std::size_t write = 0;
161 for (std::size_t i = 0; i < size_; ++i) {
162 if (records_[i].epoch != epoch_)
163 continue;
164 const auto sample = static_cast<std::uint64_t>(records_[i].absolute_sample);
165 if (sample >= end)
166 records_[write++] = records_[i];
167 }
168 size_ = write;
170 }
171
174 if (!active_)
176 active_ = false;
178 }
179
180 void reset(std::uint64_t epoch) noexcept {
181 epoch_ = epoch;
182 sample_start_ = 0;
183 frames_ = 0;
184 size_ = 0;
185 active_ = false;
186 }
187 std::uint64_t epoch() const noexcept {
188 return epoch_;
189 }
190 std::size_t pending() const noexcept {
191 return size_;
192 }
193 bool active() const noexcept {
194 return active_;
195 }
196
197 private:
198 static constexpr std::size_t kMaxFrames = 4096;
199 static bool less(const HostEventRecord& lhs, const HostEventRecord& rhs) noexcept {
200 const auto lsample = static_cast<std::uint64_t>(lhs.absolute_sample);
201 const auto rsample = static_cast<std::uint64_t>(rhs.absolute_sample);
202 if (lsample != rsample)
203 return lsample < rsample;
204 if (lhs.producer != rhs.producer)
205 return lhs.producer < rhs.producer;
206 return lhs.producer_sequence < rhs.producer_sequence;
207 }
208
209 std::array<HostEventRecord, Capacity> records_{};
210 std::uint64_t epoch_ = 0;
211 std::uint64_t sample_start_ = 0;
212 std::size_t frames_ = 0;
213 std::size_t size_ = 0;
214 bool active_ = false;
215};
216
220inline HostEventResult derive_mpe(std::span<const HostEventRecord> events,
221 std::uint64_t block_start, midi::MpeVoiceTracker& tracker,
222 midi::MpeBuffer& out) noexcept {
223 bool dropped = false;
224 int32_t offset = 0;
225 midi::bind_tracker_to_buffer(tracker, out, offset, &dropped);
226 // A bounded tracker can defer note-offs when the previous block's sidecar
227 // was full. Flush that lifecycle queue at block start before consuming new
228 // host records, matching the tracker/sidecar contract and preserving note
229 // retirement order across blocks.
230 if (!tracker.flush_pending_note_offs())
232 for (const auto& event : events) {
233 if (event.absolute_sample < 0)
235 const auto absolute = static_cast<std::uint64_t>(event.absolute_sample);
236 if (absolute < block_start ||
237 absolute - block_start >
238 static_cast<std::uint64_t>(std::numeric_limits<int32_t>::max()))
240 offset = static_cast<int32_t>(absolute - block_start);
241 if (event.has_ump)
242 (void)tracker.process(event.ump);
243 else
244 (void)tracker.process(event.midi);
245 }
247}
248
261inline HostEventResult project_host_event_block(std::span<const HostEventRecord> records,
262 std::uint64_t block_start, std::size_t frames,
263 midi::MidiBuffer& midi_out,
264 midi::MpeVoiceTracker* tracker = nullptr,
265 midi::MpeBuffer* mpe_out = nullptr) noexcept {
266 if (frames == 0 || frames > 4096 ||
267 block_start > std::numeric_limits<std::uint64_t>::max() - frames)
269 if ((tracker == nullptr) != (mpe_out == nullptr))
271
272 std::size_t midi_count = 0;
273 std::size_t ump_count = 0;
274 for (const auto& record : records) {
275 if (record.absolute_sample < 0)
277 const auto absolute = static_cast<std::uint64_t>(record.absolute_sample);
278 if (absolute < block_start || absolute >= block_start + frames ||
279 absolute - block_start >
280 static_cast<std::uint64_t>(std::numeric_limits<std::int32_t>::max()))
282 ++midi_count;
283 if (record.has_ump)
284 ++ump_count;
285 }
286 if (midi_out.realtime_capacity_limited() &&
287 midi_count >
288 midi_out.event_capacity() - std::min(midi_out.event_capacity(), midi_out.size()))
290 auto* ump = midi_out.ump();
291 if (ump_count != 0 && ump == nullptr)
293 if (ump != nullptr && ump->realtime_capacity_limited() &&
294 ump_count > ump->capacity() - std::min(ump->capacity(), ump->size()))
296 if (mpe_out != nullptr) {
297 // Channel-level MPE expression can fan out to every active note. A
298 // note lifecycle contributes at most two records (retire + replace),
299 // so this is the exact fixed upper bound for one source record. Check
300 // the bound before deriving into the caller's sidecar; otherwise a
301 // full 128-note pitch-bend fan-out could partially mutate it before
302 // reporting overflow.
303 constexpr auto kMpeRecordsPerSource = midi::MpeVoiceTracker::kMaxNotes + 2;
304 const auto event_mpe =
305 records.size() > std::numeric_limits<std::size_t>::max() / kMpeRecordsPerSource
306 ? std::numeric_limits<std::size_t>::max()
307 : records.size() * kMpeRecordsPerSource;
308 const auto pending_mpe = tracker->pending_note_off_count();
309 const auto required_mpe = pending_mpe > std::numeric_limits<std::size_t>::max() - event_mpe
310 ? std::numeric_limits<std::size_t>::max()
311 : pending_mpe + event_mpe;
312 if (required_mpe > mpe_out->capacity() - std::min(mpe_out->capacity(), mpe_out->size()))
314 }
315
316 for (const auto& record : records) {
317 auto event = record.midi;
318 event.sample_offset = static_cast<std::int32_t>(
319 static_cast<std::uint64_t>(record.absolute_sample) - block_start);
320 if (!midi_out.add(event))
322 if (record.has_ump && !ump->add(record.ump, event.sample_offset))
324 }
325 midi_out.sort();
326 if (ump != nullptr)
327 ump->sort();
328 if (tracker != nullptr && mpe_out != nullptr) {
329 const auto result = derive_mpe(records, block_start, *tracker, *mpe_out);
330 if (result != HostEventResult::Accepted)
331 return result;
332 }
334}
335
348
350 std::uint64_t epoch = 0;
351 std::uint64_t sample = 0;
352 std::uint64_t sequence = 0;
353 std::uint32_t producer = 0;
354 std::uint16_t payload = 0;
355 std::uint8_t source_port = 0;
356 std::uint8_t kind = 1;
357 friend bool operator==(const GraphAuthoredEvent&, const GraphAuthoredEvent&) = default;
358};
359static_assert(sizeof(GraphAuthoredEvent) == 32);
360
362 std::uint64_t cursor = 0;
363 std::uint64_t sequence = 0;
364 std::uint64_t epoch = 0;
365 std::uint64_t overflow = 0;
366 std::uint64_t diagnostic_refusals = 0;
367 std::uint8_t attempts = 0;
368 std::uint8_t accepted = 0;
369};
370
374template <std::size_t MaxFrames = 4096, std::size_t MaxAcceptedPerSample = 8>
376 public:
377 static constexpr std::size_t kMaxAttemptsPerSample = 16;
378 static constexpr std::size_t kRecordBytes =
379 sizeof(GraphAuthoredEvent) * MaxFrames * MaxAcceptedPerSample;
380 static_assert(kRecordBytes <= 1'048'576);
381
382 GraphEventResult reset(std::uint64_t epoch) noexcept {
383 if (epoch == 0 || active_)
385 epoch_ = epoch;
386 counters_ = {};
387 counters_.epoch = epoch;
388 cursor_ = 0;
389 sequence_ = 0;
390 trace_size_ = 0;
391 consumed_frontier_ = 0;
393 }
394
395 GraphEventResult begin_block(std::uint64_t epoch, std::uint64_t sample_start,
396 std::size_t frames) noexcept {
397 if (active_)
399 if (epoch == 0 || epoch != epoch_ || frames == 0 || frames > MaxFrames ||
400 sample_start > std::numeric_limits<std::uint64_t>::max() - frames)
402 block_start_ = sample_start;
403 block_frames_ = frames;
404 sample_offset_ = 0;
405 trace_size_ = 0;
406 active_ = true;
408 }
409
410 GraphEventResult begin_sample(std::size_t offset) noexcept {
411 if (!active_ || offset != sample_offset_ || offset >= block_frames_)
413 current_sample_ = block_start_ + offset;
414 attempts_ = accepted_ = 0;
415 attempt_closed_ = false;
416 sample_begin_ = trace_size_;
417 phase_ = 1;
419 }
420
421 GraphEventResult emit(std::uint64_t epoch, std::uint32_t producer, std::uint16_t payload,
422 std::uint64_t target_sample, std::uint8_t source_port = 0,
423 std::uint8_t kind = 1) noexcept {
424 if (attempt_closed_)
426 if (phase_ != 1) {
427 ++counters_.diagnostic_refusals;
429 }
430 if (attempts_ == kMaxAttemptsPerSample) {
431 attempt_closed_ = true;
433 }
434 ++attempts_;
435 counters_.attempts = attempts_;
436 const auto sequence = sequence_++;
437 // Expose the next producer-local sequence alongside the per-sample
438 // attempt counters. Invalid and overflowed attempts consume sequence
439 // identity, while the latched AttemptLimit refusal below does not.
440 counters_.sequence = sequence_;
441 auto refuse = [&](GraphEventResult result) {
442 ++counters_.diagnostic_refusals;
443 return result;
444 };
445 if (epoch != epoch_)
446 return refuse(GraphEventResult::StaleEpoch);
447 if (producer == 0 || source_port != 0 || kind != 1)
448 return refuse(GraphEventResult::Unbound);
449 if (target_sample < current_sample_)
450 return refuse(GraphEventResult::Late);
451 if (target_sample > current_sample_)
452 return refuse(GraphEventResult::Future);
453 if (accepted_ == MaxAcceptedPerSample) {
454 if (counters_.overflow != std::numeric_limits<std::uint64_t>::max())
455 ++counters_.overflow;
457 }
458 records_[trace_size_++] = {epoch_, current_sample_, sequence, producer,
459 payload, source_port, kind};
460 ++accepted_;
461 counters_.accepted = accepted_;
463 }
464
466 if (!active_ || phase_ != 1)
468 phase_ = 2;
470 }
471 std::span<const GraphAuthoredEvent> received() const noexcept {
472 if (phase_ != 2)
473 return {};
474 return {records_.data() + sample_begin_, trace_size_ - sample_begin_};
475 }
477 if (!active_ || phase_ != 2)
479 phase_ = 0;
480 consumed_frontier_ = current_sample_ + 1;
481 ++sample_offset_;
483 }
485 if (!active_ || phase_ != 0 || sample_offset_ != block_frames_)
487 if (cursor_ > std::numeric_limits<std::uint64_t>::max() - block_frames_) {
488 // The current block has completed its consumer phase. Retire the
489 // active state even when the monotonic cursor cannot advance, so
490 // the owner can reset to a new epoch instead of remaining wedged.
491 active_ = false;
493 }
494 cursor_ += block_frames_;
495 counters_.cursor = cursor_;
496 active_ = false;
498 }
499
500 const GraphEventCounters& counters() const noexcept {
501 return counters_;
502 }
503 std::span<const GraphAuthoredEvent> trace() const noexcept {
504 return {records_.data(), trace_size_};
505 }
506 std::uint64_t epoch() const noexcept {
507 return epoch_;
508 }
509 bool active() const noexcept {
510 return active_;
511 }
512
513 private:
515
516 void test_set_cursor_(std::uint64_t cursor) noexcept {
517 cursor_ = cursor;
518 }
519
520 std::array<GraphAuthoredEvent, MaxFrames * MaxAcceptedPerSample> records_{};
521 GraphEventCounters counters_{};
522 std::uint64_t epoch_ = 0;
523 std::uint64_t cursor_ = 0;
524 std::uint64_t sequence_ = 0;
525 std::uint64_t block_start_ = 0;
526 std::uint64_t current_sample_ = 0;
527 std::uint64_t consumed_frontier_ = 0;
528 std::size_t block_frames_ = 0;
529 std::size_t sample_offset_ = 0;
530 std::size_t sample_begin_ = 0;
531 std::size_t trace_size_ = 0;
532 std::uint8_t attempts_ = 0;
533 std::uint8_t accepted_ = 0;
534 std::uint8_t phase_ = 0;
535 bool attempt_closed_ = false;
536 bool active_ = false;
537};
538
539} // namespace pulp::host
Explicit producer/consumer phases for a bounded, single-producer event lane.
Definition graph_event_contract.hpp:375
GraphEventResult reset(std::uint64_t epoch) noexcept
Definition graph_event_contract.hpp:382
GraphEventResult end_sample() noexcept
Definition graph_event_contract.hpp:476
GraphEventResult emit(std::uint64_t epoch, std::uint32_t producer, std::uint16_t payload, std::uint64_t target_sample, std::uint8_t source_port=0, std::uint8_t kind=1) noexcept
Definition graph_event_contract.hpp:421
static constexpr std::size_t kRecordBytes
Definition graph_event_contract.hpp:378
GraphEventResult begin_consumer() noexcept
Definition graph_event_contract.hpp:465
std::span< const GraphAuthoredEvent > trace() const noexcept
Definition graph_event_contract.hpp:503
std::span< const GraphAuthoredEvent > received() const noexcept
Definition graph_event_contract.hpp:471
friend struct detail::GraphEventLaneTestAccess
Definition graph_event_contract.hpp:514
GraphEventResult begin_sample(std::size_t offset) noexcept
Definition graph_event_contract.hpp:410
GraphEventResult end_block() noexcept
Definition graph_event_contract.hpp:484
GraphEventResult begin_block(std::uint64_t epoch, std::uint64_t sample_start, std::size_t frames) noexcept
Definition graph_event_contract.hpp:395
const GraphEventCounters & counters() const noexcept
Definition graph_event_contract.hpp:500
bool active() const noexcept
Definition graph_event_contract.hpp:509
static constexpr std::size_t kMaxAttemptsPerSample
Definition graph_event_contract.hpp:377
std::uint64_t epoch() const noexcept
Definition graph_event_contract.hpp:506
Fixed-capacity host ingress staging area.
Definition graph_event_contract.hpp:65
bool active() const noexcept
Definition graph_event_contract.hpp:193
std::size_t pending() const noexcept
Definition graph_event_contract.hpp:190
std::uint64_t epoch() const noexcept
Definition graph_event_contract.hpp:187
void reset(std::uint64_t epoch) noexcept
Definition graph_event_contract.hpp:180
HostEventResult begin_block(std::uint64_t epoch, std::uint64_t sample_start, std::size_t frames) noexcept
Definition graph_event_contract.hpp:69
HostEventResult project(std::span< HostEventRecord > out, std::size_t &count) noexcept
Copy events in [sample_start, sample_start + frames) into out.
Definition graph_event_contract.hpp:127
HostEventResult end_block() noexcept
Finish the current block. Future records remain staged.
Definition graph_event_contract.hpp:173
HostEventResult publish(const HostEventRecord &event) noexcept
Definition graph_event_contract.hpp:100
Collection of timestamped MIDI events within a single audio buffer period.
Definition buffer.hpp:31
std::size_t size() const
Definition buffer.hpp:123
std::size_t event_capacity() const
Definition buffer.hpp:152
void sort()
Sort events by sample_offset for sample-accurate processing.
Definition buffer.hpp:177
bool realtime_capacity_limited() const
Definition buffer.hpp:151
const class UmpBuffer * ump() const
Attached UmpBuffer or nullptr.
Definition buffer.hpp:217
bool add(const MidiEvent &event)
Append a MIDI event to the buffer.
Definition buffer.hpp:98
A time-ordered list of per-note expression events for one block.
Definition mpe_buffer.hpp:51
Tracks MPE voice state across channels.
Definition mpe_voice_tracker.hpp:67
static constexpr std::size_t kMaxNotes
Maximum concurrently active notes the tracker can hold.
Definition mpe_voice_tracker.hpp:70
Sample-accurate MPE expression event stream.
Definition child_process_manager.hpp:18
Definition baked_codec.hpp:158
Bounded event contracts shared by host ingress and graph-authored nodes.
Definition anticipation_eligibility.hpp:9
HostEventResult
Definition graph_event_contract.hpp:29
@ Overflow
Definition graph_event_contract.hpp:35
@ OutOfRange
Definition graph_event_contract.hpp:33
@ Accepted
Definition graph_event_contract.hpp:30
@ OutputCapacity
Definition graph_event_contract.hpp:37
@ Busy
Definition graph_event_contract.hpp:38
@ Late
Definition graph_event_contract.hpp:34
@ Unbound
Definition graph_event_contract.hpp:32
@ StaleEpoch
Definition graph_event_contract.hpp:31
@ InvalidSequence
Definition graph_event_contract.hpp:36
HostEventResult project_host_event_block(std::span< const HostEventRecord > records, std::uint64_t block_start, std::size_t frames, midi::MidiBuffer &midi_out, midi::MpeVoiceTracker *tracker=nullptr, midi::MpeBuffer *mpe_out=nullptr) noexcept
Project an already-sorted absolute host-event block into the fixed MIDI, UMP, and optional MPE sideca...
Definition graph_event_contract.hpp:261
@ Busy
Definition custom_node_diagnostics.hpp:18
GraphEventResult
Definition graph_event_contract.hpp:336
@ Overflow
Definition graph_event_contract.hpp:343
@ OutOfRange
Definition graph_event_contract.hpp:340
@ Accepted
Definition graph_event_contract.hpp:337
@ InvalidPhase
Definition graph_event_contract.hpp:345
@ Future
Definition graph_event_contract.hpp:341
@ AttemptLimit
Definition graph_event_contract.hpp:344
@ Busy
Definition graph_event_contract.hpp:346
@ Late
Definition graph_event_contract.hpp:342
@ Unbound
Definition graph_event_contract.hpp:339
@ StaleEpoch
Definition graph_event_contract.hpp:338
HostEventResult derive_mpe(std::span< const HostEventRecord > events, std::uint64_t block_start, midi::MpeVoiceTracker &tracker, midi::MpeBuffer &out) noexcept
Convert projected records to the existing MPE sidecar.
Definition graph_event_contract.hpp:220
void bind_tracker_to_buffer(MpeVoiceTracker &tracker, MpeBuffer &out, int32_t &current_sample_offset, bool *expression_event_dropped=nullptr)
Convenience: install tracker callbacks that forward events to out with the given sample offset.
Definition mpe_buffer.hpp:163
Definition host_transport_projector.hpp:8
Definition graph_event_contract.hpp:349
std::uint8_t kind
Definition graph_event_contract.hpp:356
std::uint32_t producer
Definition graph_event_contract.hpp:353
std::uint8_t source_port
Definition graph_event_contract.hpp:355
std::uint64_t epoch
Definition graph_event_contract.hpp:350
std::uint64_t sequence
Definition graph_event_contract.hpp:352
friend bool operator==(const GraphAuthoredEvent &, const GraphAuthoredEvent &)=default
std::uint16_t payload
Definition graph_event_contract.hpp:354
std::uint64_t sample
Definition graph_event_contract.hpp:351
Definition graph_event_contract.hpp:361
std::uint64_t overflow
Definition graph_event_contract.hpp:365
std::uint64_t sequence
Definition graph_event_contract.hpp:363
std::uint64_t cursor
Definition graph_event_contract.hpp:362
std::uint64_t epoch
Definition graph_event_contract.hpp:364
std::uint8_t attempts
Definition graph_event_contract.hpp:367
std::uint8_t accepted
Definition graph_event_contract.hpp:368
std::uint64_t diagnostic_refusals
Definition graph_event_contract.hpp:366
One immutable host event in the graph's epoch-local sample domain.
Definition graph_event_contract.hpp:48
midi::UmpPacket ump
Definition graph_event_contract.hpp:54
midi::MidiEvent midi
Definition graph_event_contract.hpp:53
std::uint32_t producer
Definition graph_event_contract.hpp:52
std::int64_t absolute_sample
Definition graph_event_contract.hpp:50
bool has_ump
Definition graph_event_contract.hpp:55
std::uint64_t producer_sequence
Definition graph_event_contract.hpp:51
std::uint64_t epoch
Definition graph_event_contract.hpp:49
Timestamped MIDI event combining a choc::midi::ShortMessage with timing info.
Definition message.hpp:32
Definition ump.hpp:84
Sample-accurate MIDI 2.0 UMP packet stream.