Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
midi_processing.hpp
Go to the documentation of this file.
1#pragma once
2
3// Processor-facing helper for sample-accurate MIDI work.
4//
5// for_each_midi_subblock() splits a process block at MIDI-event sample
6// offsets and invokes a callback once per contiguous audio sub-block, handing
7// the callback the events that fall at that sub-block's leading boundary. It is
8// the MIDI counterpart to for_each_subblock() in param_processing.hpp: instead
9// of splitting at parameter-event offsets it splits at MIDI-event offsets, so a
10// Processor can apply events, render up to the next event, apply those, and so
11// on, without hand-rolling a per-sample event cursor.
12//
13// Realtime contract: the helper does not allocate, lock, or copy event
14// payloads on the audio thread. It walks the MidiBuffer by index and hands the
15// callback a std::span over the buffer's existing, contiguous, sorted storage.
16// Callers must sort() the MidiBuffer before calling (the buffer's sort() is
17// itself allocation-free once its scratch is reserved). prepare()/reserve()
18// work belongs off the audio thread.
19
20#include <pulp/audio/buffer.hpp>
21#include <pulp/midi/buffer.hpp>
22#include <pulp/midi/message.hpp>
23#include <cstddef>
24#include <span>
25#include <utility>
26
27namespace pulp::format {
28
59template <typename Fn>
63 Fn&& fn)
64{
65 const auto total = output.num_samples();
66 if (total == 0) return;
67
68 const std::size_t event_count = midi.size();
69
70 // The boundary walk assumes nondecreasing sample offsets (the caller must
71 // sort() first). Unsorted input is not UB here — it only mis-times events —
72 // so this guard is debug-only and compiles out of the release audio path.
73#ifndef NDEBUG
74 for (std::size_t i = 1; i < event_count; ++i) {
75 assert(midi[i].sample_offset >= midi[i - 1].sample_offset
76 && "for_each_midi_subblock requires a sorted MidiBuffer");
77 }
78#endif
79
80 auto emit = [&](std::size_t block_start,
81 std::size_t block_end,
82 std::size_t event_first,
83 std::size_t event_last) {
84 if (block_end <= block_start) return;
85 auto out_slice = output.slice(block_start, block_end - block_start);
86 auto in_slice = input.slice(block_start, block_end - block_start);
87 const auto boundary = event_last > event_first
88 ? std::span<const midi::MidiEvent>(&midi[event_first],
89 event_last - event_first)
90 : std::span<const midi::MidiEvent>{};
91 fn(out_slice, in_slice, boundary);
92 };
93
94 std::size_t start = 0; // leading sample of the current sub-block
95 std::size_t idx = 0; // next unconsumed event index
96 std::size_t boundary_first = 0; // event range applied at `start`
97
98 // Group all events with offset <= 0 onto the first sub-block boundary.
99 while (idx < event_count
100 && midi[idx].sample_offset <= 0) {
101 ++idx;
102 }
103 std::size_t boundary_last = idx;
104
105 while (idx < event_count) {
106 const auto offset = midi[idx].sample_offset;
107 if (static_cast<std::size_t>(offset) >= total) {
108 // Remaining events are at/after the block end: ignored this block.
109 break;
110 }
111
112 const auto split = static_cast<std::size_t>(offset);
113 if (split > start) {
114 emit(start, split, boundary_first, boundary_last);
115 start = split;
116 boundary_first = idx;
117 }
118
119 // Group coincident-offset events into this boundary.
120 while (idx < event_count
121 && midi[idx].sample_offset == offset) {
122 ++idx;
123 }
124 boundary_last = idx;
125 }
126
127 if (start < total) {
128 emit(start, total, boundary_first, boundary_last);
129 }
130}
131
132} // namespace pulp::format
Non-owning view over multi-channel audio sample data.
Definition buffer.hpp:30
std::size_t num_samples() const
Definition buffer.hpp:84
BufferView slice(std::size_t start, std::size_t length) const
Create a non-owning slice over this view.
Definition buffer.hpp:49
Collection of timestamped MIDI events within a single audio buffer period.
Definition buffer.hpp:31
Definition adapter_boundary.hpp:76
What the product grants a device: the quota table indexed by capability tier and live thermal state.
Definition aax_editor.hpp:31
void for_each_midi_subblock(audio::BufferView< float > &output, const audio::BufferView< const float > &input, const midi::MidiBuffer &midi, Fn &&fn)
Split a process block at MIDI-event boundaries.
Definition midi_processing.hpp:60
Definition custom_node_events.hpp:26