Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
message_collector.hpp
Go to the documentation of this file.
1#pragma once
2
28
29#include <pulp/midi/buffer.hpp>
30#include <pulp/midi/message.hpp>
32
33#include <algorithm>
34#include <array>
35#include <atomic>
36#include <cmath>
37#include <cstddef>
38#include <cstdint>
39#include <optional>
40
41namespace pulp::midi {
42
47 choc::midi::ShortMessage message{};
48 double timestamp_seconds = 0.0;
49};
50
52 std::size_t queue_size_approx = 0;
53 std::size_t queue_capacity = 0;
54 std::uint64_t queue_overflow_count = 0;
55 std::size_t pending_size_approx = 0;
56 std::size_t pending_capacity = 0;
57 std::size_t dropped_future = 0;
58};
59
60template<std::size_t Capacity = 256>
62public:
64
68 bool push_now(const MidiEvent& event, double now_seconds) {
69 TimestampedShortMessage entry{event.message, now_seconds};
70 return queue_.try_push(entry);
71 }
72
74 bool push_now(const MidiEvent& event) {
75 return push_now(event, event.timestamp);
76 }
77
91 std::size_t drain_into(MidiBuffer& out,
92 double block_start_seconds,
93 int block_size_samples,
94 double sample_rate) {
95 if (block_size_samples <= 0 || sample_rate <= 0.0) return 0;
96 const double block_duration = block_size_samples / sample_rate;
97 const double block_end_seconds = block_start_seconds + block_duration;
98
99 std::size_t drained = 0;
100
101 auto deliver = [&](const TimestampedShortMessage& entry) {
102 int sample_offset = 0;
103 if (entry.timestamp_seconds > block_start_seconds) {
104 const double offset_seconds = entry.timestamp_seconds - block_start_seconds;
105 // Round to nearest sample — truncation would lose
106 // ~1 sample on every tick whose product is not exactly
107 // representable in binary float.
108 sample_offset = static_cast<int>(std::lround(offset_seconds * sample_rate));
109 sample_offset = std::clamp(sample_offset, 0, block_size_samples - 1);
110 }
111 MidiEvent event{entry.message, sample_offset, entry.timestamp_seconds};
112 out.add(event);
113 };
114
115 // Walk the consumer-owned `pending_` ring first: any deferred-
116 // future entry whose timestamp now fits the current block is
117 // delivered. Entries that are still in the future stay put so
118 // a later block can consume them. Keeping these events out of
119 // the producer queue ensures the consumer thread never writes
120 // back to the SPSC FIFO it is draining.
121 for (auto& slot : pending_) {
122 if (slot.has_value() && slot->timestamp_seconds < block_end_seconds) {
123 deliver(*slot);
124 slot.reset();
125 ++drained;
126 }
127 }
128
129 // Drain the queue. Events that fit the current block are
130 // delivered immediately; future events are stashed in the
131 // consumer-owned pending ring. Draining continues after a
132 // future event lands so later in-block events still get
133 // delivered when producer timestamps arrive out of order.
134 //
135 // The pending ring is sized (`kPendingSlots`) to absorb the
136 // realistic Pulp workloads in one drain: UI clicks, scripting
137 // schedulers with look-ahead, MIDI file playback. When the
138 // ring is genuinely saturated, additional FUTURE events have
139 // nowhere to live and are dropped — observable via the atomic
140 // `dropped_future_` counter so callers can detect pathological
141 // back-pressure.
142 while (auto next = queue_.try_pop()) {
143 const auto& entry = *next;
144 if (entry.timestamp_seconds < block_end_seconds) {
145 deliver(entry);
146 ++drained;
147 continue;
148 }
149 // Future event: try to stash in the pending ring.
150 if (stash_pending(entry)) {
151 continue;
152 }
153 // Ring genuinely saturated. Drop with atomic counter.
154 dropped_future_.fetch_add(1, std::memory_order_relaxed);
155 }
156 return drained;
157 }
158
166 std::size_t dropped_future() const {
167 return dropped_future_.load(std::memory_order_relaxed);
168 }
169
173 std::uint64_t queue_overflow_count() const {
174 return queue_.overflow_count();
175 }
176
181 queue_.reset_overflow_count();
182 }
183
188 const auto queue = queue_.telemetry();
189 return {
190 .queue_size_approx = queue.size_approx,
191 .queue_capacity = queue.capacity,
192 .queue_overflow_count = queue.overflow_count,
193 .pending_size_approx = pending_size_approx(),
194 .pending_capacity = pending_capacity(),
195 .dropped_future = dropped_future(),
196 };
197 }
198
202 std::size_t size_approx() const { return queue_.size_approx(); }
203
205 static constexpr std::size_t capacity() { return Capacity; }
206
214 static constexpr std::size_t pending_capacity() { return kPendingSlots; }
215
216private:
217 static constexpr std::size_t kPendingSlots = 64;
218
223 std::array<std::optional<TimestampedShortMessage>, kPendingSlots> pending_{};
224 std::atomic<std::size_t> dropped_future_{0};
225
226 std::size_t pending_size_approx() const {
227 std::size_t count = 0;
228 for (const auto& slot : pending_) {
229 if (slot.has_value()) ++count;
230 }
231 return count;
232 }
233
239 bool stash_pending(const TimestampedShortMessage& entry) {
240 for (auto& slot : pending_) {
241 if (!slot.has_value()) {
242 slot = entry;
243 return true;
244 }
245 }
246 return false; // all slots full
247 }
248};
249
250} // namespace pulp::midi
Collection of timestamped MIDI events within a single audio buffer period.
Definition buffer.hpp:31
bool add(const MidiEvent &event)
Append a MIDI event to the buffer.
Definition buffer.hpp:98
std::size_t size_approx() const
Approximate number of events currently queued (does NOT count the deferred-future entries in the pend...
Definition message_collector.hpp:202
bool push_now(const MidiEvent &event)
Convenience overload using MidiEvent::timestamp directly.
Definition message_collector.hpp:74
std::size_t dropped_future() const
Number of FUTURE events the consumer dropped because the pending ring was saturated at the moment the...
Definition message_collector.hpp:166
std::size_t drain_into(MidiBuffer &out, double block_start_seconds, int block_size_samples, double sample_rate)
Audio / consumer thread — drain queued events into out at the correct sample offsets within the curre...
Definition message_collector.hpp:91
bool push_now(const MidiEvent &event, double now_seconds)
UI / producer thread — push a MidiEvent timestamped at now_seconds.
Definition message_collector.hpp:68
static constexpr std::size_t pending_capacity()
Compile-time size of the consumer-owned pending ring.
Definition message_collector.hpp:214
static constexpr std::size_t capacity()
Compile-time capacity of the producer queue.
Definition message_collector.hpp:205
std::uint64_t queue_overflow_count() const
Producer-side queue overflow count from failed push_now() attempts.
Definition message_collector.hpp:173
MidiMessageCollectorTelemetry telemetry() const
Cheap snapshot for UI, validation, and inspector polling.
Definition message_collector.hpp:187
void reset_queue_overflow_count()
Reset only the producer queue overflow counter.
Definition message_collector.hpp:180
Definition spsc_queue.hpp:23
@ count
sentinel: number of real capabilities (keep LAST)
Definition canvas_capability.hpp:38
Definition custom_node_events.hpp:26
Timestamped MIDI event combining a choc::midi::ShortMessage with timing info.
Definition message.hpp:32
double timestamp
Absolute time in seconds (for device I/O).
Definition message.hpp:35
Definition message_collector.hpp:51
std::size_t pending_size_approx
Definition message_collector.hpp:55
std::size_t queue_size_approx
Definition message_collector.hpp:52
std::uint64_t queue_overflow_count
Definition message_collector.hpp:54
std::size_t dropped_future
Definition message_collector.hpp:57
std::size_t queue_capacity
Definition message_collector.hpp:53
std::size_t pending_capacity
Definition message_collector.hpp:56
Timestamped MIDI event in the collector queue.
Definition message_collector.hpp:46
choc::midi::ShortMessage message
Definition message_collector.hpp:47
double timestamp_seconds
Definition message_collector.hpp:48