Zrythm v2.0.0-alpha.1+31.4967fd053471
a highly automated and intuitive digital audio workstation
Loading...
Searching...
No Matches
midi_event_buffer.h
1// SPDX-FileCopyrightText: © 2026 Alexandros Theodotou <alex@zrythm.org>
2// SPDX-License-Identifier: LicenseRef-ZrythmLicense
3
4#pragma once
5
6#include <algorithm>
7#include <cassert>
8#include <cstddef>
9#include <cstdint>
10#include <cstring>
11#include <iterator>
12#include <limits>
13#include <span>
14#include <vector>
15
16#include "utils/midi.h"
17#include "utils/units.h"
18
19namespace zrythm::dsp
20{
21
29struct MidiEventView
30{
32 units::sample_u32_t time () const noexcept [[clang::nonblocking]]
33 {
34 return units::samples (time_raw_);
35 }
36
37 std::span<const midi_byte_t> data () const noexcept [[clang::nonblocking]]
38 {
39 return { data_, size_ };
40 }
41
42private:
43 friend class MidiEventBuffer;
44
45 MidiEventView (uint32_t t, const midi_byte_t * d, uint16_t s) noexcept
46 : time_raw_ (t), data_ (d), size_ (s)
47 {
48 }
49
50 uint32_t time_raw_{};
51 const midi_byte_t * data_{ nullptr };
52 uint16_t size_{ 0 };
53};
54
74{
75public:
77 static constexpr size_t kHeaderSize = sizeof (uint16_t) + sizeof (uint32_t);
78
80 static constexpr size_t kDefaultReserveBytes = 0x1000;
81
83 static constexpr size_t kMaxReserveBytes = 0x4000;
84
91 [[nodiscard]] static MidiEventBuffer make_reserved ()
92 {
95 return buf;
96 }
97
104 class Iterator
105 {
106 public:
107 using iterator_category = std::forward_iterator_tag;
108 using value_type = MidiEventView;
109 using difference_type = std::ptrdiff_t;
110 using pointer = void;
111 using reference = MidiEventView;
112
113 Iterator () = default;
114 Iterator (const midi_byte_t * ptr, const midi_byte_t * end) noexcept
115 : ptr_ (ptr), end_ (end)
116 {
117 }
118
119 MidiEventView operator* () const noexcept
120 {
121 assert (ptr_ != nullptr && ptr_ < end_);
122 uint16_t size{};
123 std::memcpy (&size, ptr_, sizeof (uint16_t));
124 uint32_t time_raw{};
125 std::memcpy (&time_raw, ptr_ + sizeof (uint16_t), sizeof (uint32_t));
126 return MidiEventView{ time_raw, ptr_ + kHeaderSize, size };
127 }
128
129 Iterator &operator++ () noexcept
130 {
131 assert (ptr_ != nullptr && ptr_ < end_);
132 uint16_t size{};
133 std::memcpy (&size, ptr_, sizeof (uint16_t));
134 ptr_ += kHeaderSize + size;
135 return *this;
136 }
137
138 Iterator operator++ (int) noexcept
139 {
140 Iterator tmp = *this;
141 ++(*this);
142 return tmp;
143 }
144
145 bool operator== (const Iterator &other) const noexcept
146 {
147 return ptr_ == other.ptr_;
148 }
149
150 private:
151 const midi_byte_t * ptr_ = nullptr;
152 const midi_byte_t * end_ = nullptr;
153 };
154
166 units::sample_u32_t time,
167 std::span<const midi_byte_t> event_data) noexcept [[clang::nonblocking]]
168 {
169 assert (event_data.size () <= std::numeric_limits<uint16_t>::max ());
170 const auto total = kHeaderSize + event_data.size ();
171 const auto old_size = storage_.size ();
172 assert (old_size + total <= storage_.capacity ());
173 if (old_size + total > storage_.capacity ())
174 return false;
175 storage_.resize (old_size + total);
176
177 const auto time_raw = time.in<uint32_t> (units::samples);
178 auto * ptr = storage_.data () + old_size;
179 auto sz = static_cast<uint16_t> (event_data.size ());
180 std::memcpy (ptr, &sz, sizeof (uint16_t));
181 std::memcpy (ptr + sizeof (uint16_t), &time_raw, sizeof (uint32_t));
182 std::memcpy (ptr + kHeaderSize, event_data.data (), event_data.size ());
183 ++event_count_;
184 return true;
185 }
186
188 void clear () noexcept [[clang::nonblocking]]
189 {
190 storage_.clear ();
191 event_count_ = 0;
192 }
193
195 void reserve (size_t bytes = kMaxReserveBytes)
196 {
197 storage_.reserve (bytes);
198 scratch_.reserve (bytes);
199 // Worst case: 1 data byte per event (e.g., MIDI clock 0xF8).
200 index_.reserve (bytes / (kHeaderSize + 1));
201 }
202
203 void swap (MidiEventBuffer &other) noexcept
204 {
205 storage_.swap (other.storage_);
206 scratch_.swap (other.scratch_);
207 index_.swap (other.index_);
208 std::swap (event_count_, other.event_count_);
209 }
210
211 bool empty () const noexcept { return event_count_ == 0; }
213 size_t size () const noexcept { return event_count_; }
215 size_t size_in_bytes () const noexcept { return storage_.size (); }
217 size_t capacity () const noexcept { return storage_.capacity (); }
218
219 MidiEventView front () const noexcept
220 {
221 assert (!empty ());
222 return *begin ();
223 }
224
225 Iterator begin () const noexcept
226 {
227 return Iterator (storage_.data (), storage_.data () + storage_.size ());
228 }
229
230 Iterator end () const noexcept
231 {
232 const auto * end_ptr = storage_.data () + storage_.size ();
233 return Iterator (end_ptr, end_ptr);
234 }
235
241 template <typename Comp>
242 void sort (Comp &&comp) noexcept [[clang::nonblocking]]
243 {
244 if (event_count_ <= 1)
245 return;
246
247 assert (storage_.size () <= scratch_.capacity ());
248 scratch_.resize (storage_.size ());
249 assert (event_count_ <= index_.capacity ());
250 index_.clear ();
251
252 size_t offset = 0;
253 while (offset < storage_.size ())
254 {
255 uint16_t size{};
256 std::memcpy (&size, storage_.data () + offset, sizeof (uint16_t));
257 uint32_t time_raw{};
258 std::memcpy (
259 &time_raw, storage_.data () + offset + sizeof (uint16_t),
260 sizeof (uint32_t));
261 assert (index_.size () < index_.capacity ());
262 index_.push_back ({ offset, time_raw, size });
263 offset += kHeaderSize + size;
264 }
265
266 const auto * storage_ptr = storage_.data ();
267 std::ranges::sort (
268 index_, [storage_ptr, &comp] (const IndexEntry &a, const IndexEntry &b) {
269 MidiEventView va{
270 a.time_raw, storage_ptr + a.offset + kHeaderSize, a.size
271 };
272 MidiEventView vb{
273 b.time_raw, storage_ptr + b.offset + kHeaderSize, b.size
274 };
275 return comp (va, vb);
276 });
277
278 size_t write_pos = 0;
279 for (const auto &entry : index_)
280 {
281 const auto chunk = kHeaderSize + entry.size;
282 std::memcpy (
283 scratch_.data () + write_pos, storage_.data () + entry.offset, chunk);
284 write_pos += chunk;
285 }
286
287 scratch_.resize (write_pos);
288 storage_.swap (scratch_);
289 }
290
292 template <typename Pred>
293 void remove_if (Pred &&pred) noexcept [[clang::nonblocking]]
294 {
295 if (empty ())
296 return;
297
298 assert (storage_.size () <= scratch_.capacity ());
299 scratch_.resize (storage_.size ());
300
301 size_t write_pos = 0;
302 size_t new_count = 0;
303 const auto * read_ptr = storage_.data ();
304 const auto * end_ptr = storage_.data () + storage_.size ();
305
306 while (read_ptr < end_ptr)
307 {
308 uint16_t size{};
309 std::memcpy (&size, read_ptr, sizeof (uint16_t));
310 uint32_t time_raw{};
311 std::memcpy (&time_raw, read_ptr + sizeof (uint16_t), sizeof (uint32_t));
312
313 MidiEventView view{ time_raw, read_ptr + kHeaderSize, size };
314
315 if (!pred (view))
316 {
317 const auto chunk = kHeaderSize + size;
318 std::memcpy (scratch_.data () + write_pos, read_ptr, chunk);
319 write_pos += chunk;
320 ++new_count;
321 }
322
323 read_ptr += kHeaderSize + size;
324 }
325
326 if (new_count == event_count_)
327 return;
328
329 scratch_.resize (write_pos);
330 storage_.swap (scratch_);
331 event_count_ = new_count;
332 }
333
334private:
335 struct IndexEntry
336 {
337 size_t offset{};
338 uint32_t time_raw{};
339 uint16_t size{};
340 };
341
342 std::vector<midi_byte_t> storage_;
343 std::vector<midi_byte_t> scratch_;
344 std::vector<IndexEntry> index_;
345 size_t event_count_ = 0;
346};
347
348namespace midi_event
349{
350
351inline void
352sort_with_note_off_priority (MidiEventBuffer &buf) noexcept
353 [[clang::nonblocking]]
354{
355 buf.sort ([] (const MidiEventView &a, const MidiEventView &b) {
356 if (a.time () != b.time ())
357 return a.time () < b.time ();
358 return !utils::midi::midi_is_note_on (a.data ())
359 && utils::midi::midi_is_note_on (b.data ());
360 });
361}
362
367inline void
368append_in_range (
369 MidiEventBuffer &dst,
370 const MidiEventBuffer &src,
371 std::pair<units::sample_u32_t, units::sample_u32_t> range) noexcept
372 [[clang::nonblocking]]
373{
374 for (const auto &ev : src)
375 {
376 if (ev.time () >= range.first && ev.time () < range.second)
377 dst.push_back (ev.time (), ev.data ());
378 }
379}
380
381} // namespace midi_event
382
383} // namespace zrythm::dsp
Packed contiguous buffer for RT MIDI events.
void remove_if(Pred &&pred) noexcept
Remove all events matching the predicate.
void sort(Comp &&comp) noexcept
Sort events using the given comparator on MidiEventView.
size_t size_in_bytes() const noexcept
Bytes used in storage.
static constexpr size_t kMaxReserveBytes
Max bytes to hold in queues.
static constexpr size_t kDefaultReserveBytes
Default pre-allocation.
size_t capacity() const noexcept
Bytes reserved in storage.
static constexpr size_t kHeaderSize
Bytes per event header: 2 (size) + 4 (timestamp).
size_t size() const noexcept
Number of events (not bytes).
static MidiEventBuffer make_reserved()
Create a pre-allocated buffer suitable for test use.
void reserve(size_t bytes=kMaxReserveBytes)
Pre-allocate internal byte storage (and scratch/index buffers).
void clear() noexcept
Reset buffer.
bool push_back(units::sample_u32_t time, std::span< const midi_byte_t > event_data) noexcept
Append an event.
MIDI utils.
std::uint8_t midi_byte_t
MIDI byte.
Definition midi.h:43
Factory functions and algorithms for MidiEvent containers.
Definition midi_event.h:140
void sort_with_note_off_priority(Container &container)
Sorts events by time, with noteOff before noteOn at the same timestamp.
Definition midi_event.h:340
Non-owning view into a MIDI event stored in a MidiEventBuffer.
units::sample_u32_t time() const noexcept
Event timestamp as a sample offset within the current processing cycle.