Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
mpe_buffer.hpp
Go to the documentation of this file.
1#pragma once
2
15
17#include <algorithm>
18#include <array>
19#include <cstdint>
20#include <limits>
21#include <span>
22#include <vector>
23
24namespace pulp::midi {
25
44
51class MpeBuffer {
52 public:
53 // Graph runtime scratch starts empty and is reserved by its off-RT prepare
54 // step. Standalone adapter buffers retain the historical eager capacity.
55 explicit MpeBuffer(bool reserve_storage = true) {
56 if (reserve_storage)
57 reserve(kInitialCapacity);
58 }
59
60 bool add(const MpeExpressionEvent& e) {
61 if (!can_append()) {
62 record_drop();
63 return false;
64 }
65 events_.push_back(e);
66 return true;
67 }
69 if (!can_append()) {
70 record_drop();
71 return false;
72 }
73 events_.push_back(std::move(e));
74 return true;
75 }
76
80 bool add_batch(std::span<const MpeExpressionEvent> events) {
81 if (!can_append(events.size())) {
82 record_drops(events.size());
83 return false;
84 }
85 events_.insert(events_.end(), events.begin(), events.end());
86 return true;
87 }
88
89 void clear() {
90 events_.clear();
91 dropped_events_ = 0;
92 }
93 bool empty() const { return events_.empty(); }
94 std::size_t size() const { return events_.size(); }
95 std::size_t capacity() const { return events_.capacity(); }
96 std::uint32_t dropped_event_count() const { return dropped_events_; }
97 void reserve(std::size_t capacity) {
98 events_.reserve(capacity);
99 sort_index_.reserve(capacity);
100 sort_reorder_.reserve(capacity);
101 }
102 void set_realtime_capacity_limit(bool enabled = true) {
103 limit_to_reserved_capacity_ = enabled;
104 }
105
106 void sort() {
107 // Match MidiBuffer's allocation-free insertion-stable index sort.
108 // Producer order at equal offsets is semantic for retriggers:
109 // retirement must remain immediately before the replacement note-on.
110 const std::size_t count = events_.size();
111 if (count < 2) return;
112 sort_index_.resize(count);
113 for (std::size_t i = 0; i < count; ++i) sort_index_[i] = i;
114 const auto& source = events_;
115 std::sort(sort_index_.begin(), sort_index_.end(),
116 [&source](std::size_t a, std::size_t b) {
117 if (source[a].sample_offset != source[b].sample_offset) {
118 return source[a].sample_offset < source[b].sample_offset;
119 }
120 return a < b;
121 });
122 sort_reorder_.resize(count);
123 for (std::size_t i = 0; i < count; ++i) {
124 sort_reorder_[i] = std::move(events_[sort_index_[i]]);
125 }
126 events_.swap(sort_reorder_);
127 }
128
129 auto begin() { return events_.begin(); }
130 auto end() { return events_.end(); }
131 auto begin() const { return events_.begin(); }
132 auto end() const { return events_.end(); }
133
134 const MpeExpressionEvent& operator[](std::size_t i) const { return events_[i]; }
135
136private:
137 bool can_append(std::size_t count = 1) const {
138 return !limit_to_reserved_capacity_
139 || count <= events_.capacity() - events_.size();
140 }
141 void record_drop() {
142 record_drops(1);
143 }
144 void record_drops(std::size_t count) {
145 const auto max = std::numeric_limits<std::uint32_t>::max();
146 const auto available = static_cast<std::uint64_t>(max - dropped_events_);
147 const auto increment = std::min<std::uint64_t>(count, available);
148 dropped_events_ += static_cast<std::uint32_t>(increment);
149 }
150
151 static constexpr std::size_t kInitialCapacity = 128;
152 std::vector<MpeExpressionEvent> events_;
153 std::vector<std::size_t> sort_index_;
154 std::vector<MpeExpressionEvent> sort_reorder_;
155 bool limit_to_reserved_capacity_ = false;
156 std::uint32_t dropped_events_ = 0;
157};
158
164 MpeBuffer& out,
165 int32_t& current_sample_offset,
166 bool* expression_event_dropped = nullptr) {
167 using K = MpeExpressionEvent::Kind;
168 tracker.on_note_lifecycle = [&out, &current_sample_offset,
169 expression_event_dropped](
170 const MpeNoteState* note_off, const MpeNoteState* note_on) {
171 std::array<MpeExpressionEvent, 2> events{};
172 std::size_t count = 0;
173 if (note_off) {
174 events[count++] = {current_sample_offset, K::NoteOff, *note_off};
175 }
176 if (note_on) {
177 events[count++] = {current_sample_offset, K::NoteOn, *note_on};
178 }
179 const bool appended = out.add_batch(
180 std::span<const MpeExpressionEvent>{events.data(), count});
181 // A rejected start/retrigger would let raw MIDI advance without the
182 // matching MPE lifecycle. Pure releases instead use the tracker's
183 // bounded deferred-release queue and are flushed at the next block.
184 if (!appended && note_on && expression_event_dropped)
185 *expression_event_dropped = true;
186 return appended;
187 };
188 tracker.on_pitch_bend = [&out, &current_sample_offset,
189 expression_event_dropped](const MpeNoteState& s) {
190 if (!out.add({current_sample_offset, K::PitchBend, s}) &&
191 expression_event_dropped) {
192 *expression_event_dropped = true;
193 }
194 };
195 tracker.on_pressure = [&out, &current_sample_offset,
196 expression_event_dropped](const MpeNoteState& s) {
197 if (!out.add({current_sample_offset, K::Pressure, s}) &&
198 expression_event_dropped) {
199 *expression_event_dropped = true;
200 }
201 };
202 tracker.on_timbre = [&out, &current_sample_offset,
203 expression_event_dropped](const MpeNoteState& s) {
204 if (!out.add({current_sample_offset, K::Timbre, s}) &&
205 expression_event_dropped) {
206 *expression_event_dropped = true;
207 }
208 };
209}
210
211} // namespace pulp::midi
A time-ordered list of per-note expression events for one block.
Definition mpe_buffer.hpp:51
bool add(MpeExpressionEvent &&e)
Definition mpe_buffer.hpp:68
bool add(const MpeExpressionEvent &e)
Definition mpe_buffer.hpp:60
bool empty() const
Definition mpe_buffer.hpp:93
const MpeExpressionEvent & operator[](std::size_t i) const
Definition mpe_buffer.hpp:134
auto end() const
Definition mpe_buffer.hpp:132
auto end()
Definition mpe_buffer.hpp:130
auto begin() const
Definition mpe_buffer.hpp:131
auto begin()
Definition mpe_buffer.hpp:129
std::size_t size() const
Definition mpe_buffer.hpp:94
std::size_t capacity() const
Definition mpe_buffer.hpp:95
void clear()
Definition mpe_buffer.hpp:89
void set_realtime_capacity_limit(bool enabled=true)
Definition mpe_buffer.hpp:102
MpeBuffer(bool reserve_storage=true)
Definition mpe_buffer.hpp:55
void sort()
Definition mpe_buffer.hpp:106
bool add_batch(std::span< const MpeExpressionEvent > events)
Append a group atomically.
Definition mpe_buffer.hpp:80
void reserve(std::size_t capacity)
Definition mpe_buffer.hpp:97
std::uint32_t dropped_event_count() const
Definition mpe_buffer.hpp:96
Tracks MPE voice state across channels.
Definition mpe_voice_tracker.hpp:67
std::function< void(const MpeNoteState &)> on_pressure
Definition mpe_voice_tracker.hpp:238
std::function< void(const MpeNoteState &)> on_pitch_bend
Definition mpe_voice_tracker.hpp:237
std::function< void(const MpeNoteState &)> on_timbre
Definition mpe_voice_tracker.hpp:239
std::function< bool(const MpeNoteState *note_off, const MpeNoteState *note_on)> on_note_lifecycle
Transactional note-lifecycle sink.
Definition mpe_voice_tracker.hpp:233
MPE (MIDI Polyphonic Expression) voice tracker.
Definition child_process_manager.hpp:18
Definition custom_node_events.hpp:26
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
A single per-note expression event.
Definition mpe_buffer.hpp:31
Kind
Definition mpe_buffer.hpp:32
@ NoteOn
Definition mpe_buffer.hpp:33
@ NoteOff
Definition mpe_buffer.hpp:34
@ Pressure
Definition mpe_buffer.hpp:36
@ PitchBend
Definition mpe_buffer.hpp:35
@ Timbre
Definition mpe_buffer.hpp:37
Kind kind
Definition mpe_buffer.hpp:41
int32_t sample_offset
Sample position within the current block.
Definition mpe_buffer.hpp:40
MpeNoteState state
Snapshot at event time.
Definition mpe_buffer.hpp:42
Per-note state maintained by MpeVoiceTracker.
Definition mpe_voice_tracker.hpp:44