Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
raw_midi_parser.hpp
Go to the documentation of this file.
1// Raw-MIDI byte-stream parser — pulls short messages, sysex, and
2// realtime bytes out of a best-effort byte stream.
3//
4// Extracted into a pure helper so ALSA raw-midi, WinMM MIM_LONGDATA,
5// and any future raw-byte MIDI transport can share the same
6// accumulator logic and — more importantly — share a test suite.
7//
8// Not a public API; consumed from:
9// core/midi/platform/linux/alsa_midi_device.cpp
10// test/test_raw_midi_parser.cpp
11//
12// Design notes
13// ────────────
14// Sysex (F0..F7) is variable-length and can span multiple read() calls,
15// so the accumulator state (sysex_buffer / sysex_in_progress) lives on
16// the caller's RawMidiParserState and survives between invocations.
17//
18// MIDI 1.0 §3.1 allows real-time bytes (F8..FF) inside a sysex; they
19// are emitted inline and do NOT terminate the accumulator.
20//
21// A non-realtime status byte (0x80..0xF7, excluding 0xF7 itself which
22// terminates sysex) arriving mid-sysex means the device aborted the
23// F0 stream without sending F7 — common on disconnect or firmware
24// glitches. We drop the partial buffer and parse that status byte
25// as the start of a new message, rather than silently consuming the
26// next real message as SysEx payload.
27//
28// RT-safety contract:
29//
30// - Call RawMidiParserState::reserve_sysex(N) before entering the MIDI
31// callback, where N is the largest F0...F7 payload the transport accepts.
32// - After that preparation, parse_raw_midi_bytes() does not allocate while
33// SysEx payloads stay within the reserved size.
34// - Callback objects and callback-owned destination storage are caller-owned.
35// Keep them preconstructed and allocation-free when using this helper from
36// realtime code.
37
38#pragma once
39
40#include <cstddef>
41#include <cstdint>
42#include <functional>
43#include <vector>
44
45namespace pulp::midi {
46
47// Typical device sysex dumps fit comfortably under 64 KB. If the
48// accumulator collects more without seeing F7, assume the device is
49// misbehaving and drop the buffer to cap memory use.
50inline constexpr std::size_t kRawMidiSysexLimit = 64 * 1024;
51
54
60 void reserve_sysex(std::size_t worst_case_bytes) {
62 maximum_sysex_bytes = worst_case_bytes;
63 sysex_buffer.reserve(worst_case_bytes);
64 }
65
67 void reset_sysex() noexcept {
68 sysex_buffer.clear();
69 sysex_in_progress = false;
71 }
72
73 std::vector<uint8_t> sysex_buffer;
74 bool sysex_in_progress = false;
75 uint8_t running_status = 0;
76 uint8_t pending_status = 0;
77 uint8_t pending_data[2] = {};
78 std::size_t pending_count = 0;
79 std::size_t pending_expected = 0;
80
83};
84
85// Short message = status byte + 0..2 data bytes (channel voice /
86// system common, excluding realtime and sysex).
88 std::function<void(uint8_t status, uint8_t d1, uint8_t d2)>;
89
90// Sysex callback fires once per complete F0..F7 run. The buffer passed
91// in includes the leading F0 and trailing F7.
93 std::function<void(const std::vector<uint8_t>& buf)>;
94
95// Feed `n` bytes from `buf` into the parser. `state` holds cross-call
96// sysex accumulator state and must be preserved between calls. `on_short`
97// and `on_sysex` may be empty; callers that don't care about either
98// stream pass a null std::function.
99inline void parse_raw_midi_bytes(const uint8_t* buf,
100 std::size_t n,
102 const RawMidiShortCallback& on_short,
103 const RawMidiSysexCallback& on_sysex) {
104 auto clear_pending = [&state] {
105 state.pending_status = 0;
106 state.pending_data[0] = 0;
107 state.pending_data[1] = 0;
108 state.pending_count = 0;
109 state.pending_expected = 0;
110 };
111
112 auto short_data_len = [](uint8_t status) -> std::size_t {
113 if ((status & 0xF0) >= 0x80 && (status & 0xF0) <= 0xE0) {
114 return ((status & 0xF0) == 0xC0 || (status & 0xF0) == 0xD0) ? 1u : 2u;
115 }
116 if (status == 0xF1 || status == 0xF3) return 1;
117 if (status == 0xF2) return 2;
118 if (status == 0xF6 || status == 0xF7) return 0;
119 return static_cast<std::size_t>(-1);
120 };
121
122 auto is_channel_status = [](uint8_t status) {
123 return (status & 0xF0) >= 0x80 && (status & 0xF0) <= 0xE0;
124 };
125
126 auto emit_pending = [&] {
127 if (on_short) {
128 on_short(state.pending_status,
129 state.pending_expected > 0 ? state.pending_data[0] : 0,
130 state.pending_expected > 1 ? state.pending_data[1] : 0);
131 }
132 const uint8_t completed_status = state.pending_status;
133 clear_pending();
134 if (is_channel_status(completed_status)) state.running_status = completed_status;
135 };
136
137 auto start_pending = [&](uint8_t status) {
138 clear_pending();
139 const auto data_len = short_data_len(status);
140 if (data_len == static_cast<std::size_t>(-1)) {
141 state.running_status = 0;
142 return;
143 }
144 if (is_channel_status(status)) {
145 state.running_status = status;
146 } else {
147 state.running_status = 0;
148 }
149 if (data_len == 0) {
150 if (on_short) on_short(status, 0, 0);
151 return;
152 }
153 state.pending_status = status;
154 state.pending_expected = data_len;
155 };
156
157 auto consume_data = [&](uint8_t data) {
158 if (state.pending_status == 0) {
159 if (state.running_status == 0) return;
160 start_pending(state.running_status);
161 }
162 if (state.pending_status == 0 || state.pending_count >= 2) return;
163 state.pending_data[state.pending_count++] = data;
164 if (state.pending_count >= state.pending_expected) emit_pending();
165 };
166
167 for (std::size_t i = 0; i < n; ++i) {
168 const uint8_t b = buf[i];
169
170 // Real-time (F8..FF) passes through unconditionally, even
171 // inside a sysex run.
172 if (b >= 0xF8) {
173 if (on_short) on_short(b, 0, 0);
174 continue;
175 }
176
177 if (state.sysex_in_progress) {
178 if (b == 0xF7) {
179 if (state.sysex_buffer.size() >= state.maximum_sysex_bytes) {
180 state.sysex_buffer.clear();
181 state.sysex_in_progress = false;
183 continue;
184 }
185 state.sysex_buffer.push_back(b);
186 if (on_sysex) on_sysex(state.sysex_buffer);
187 state.sysex_buffer.clear();
188 state.sysex_in_progress = false;
190 continue;
191 }
192 // A new non-realtime status byte means the F0 stream was
193 // aborted. Drop the partial buffer and fall through so
194 // this byte can be parsed as the start of a new message.
195 if ((b & 0x80) != 0) {
196 state.sysex_buffer.clear();
197 state.sysex_in_progress = false;
199 // fall through
200 } else {
201 if (state.sysex_buffer.size() >= state.maximum_sysex_bytes) {
202 state.sysex_buffer.clear();
203 state.sysex_in_progress = false;
205 continue;
206 }
207 state.sysex_buffer.push_back(b);
208 continue;
209 }
210 }
211
212 if (b == 0xF0) {
213 state.sysex_buffer.clear();
214 state.running_status = 0;
215 clear_pending();
216 if (state.maximum_sysex_bytes == 0) {
217 state.sysex_in_progress = false;
219 continue;
220 }
221 state.sysex_buffer.push_back(b);
222 state.sysex_in_progress = true;
224 continue;
225 }
226
227 if ((b & 0x80) != 0) {
228 start_pending(b);
229 continue;
230 }
231
232 consume_data(b);
233 }
234}
235
236} // namespace pulp::midi
Definition custom_node_events.hpp:26
constexpr std::size_t kRawMidiSysexLimit
Definition raw_midi_parser.hpp:50
void parse_raw_midi_bytes(const uint8_t *buf, std::size_t n, RawMidiParserState &state, const RawMidiShortCallback &on_short, const RawMidiSysexCallback &on_sysex)
Definition raw_midi_parser.hpp:99
std::function< void(const std::vector< uint8_t > &buf)> RawMidiSysexCallback
Definition raw_midi_parser.hpp:92
std::function< void(uint8_t status, uint8_t d1, uint8_t d2)> RawMidiShortCallback
Definition raw_midi_parser.hpp:87
SequencerStateChannel is the audio-safe transport for a plugin's structured NON-parameter state: the ...
Definition audio_device_manager.hpp:48
Definition raw_midi_parser.hpp:52
std::size_t pending_count
Definition raw_midi_parser.hpp:78
DropReason last_drop_reason
Definition raw_midi_parser.hpp:82
std::size_t maximum_sysex_bytes
Definition raw_midi_parser.hpp:81
std::size_t pending_expected
Definition raw_midi_parser.hpp:79
bool sysex_in_progress
Definition raw_midi_parser.hpp:74
uint8_t pending_data[2]
Definition raw_midi_parser.hpp:77
void reset_sysex() noexcept
Discard an incomplete message while retaining prepared capacity.
Definition raw_midi_parser.hpp:67
uint8_t running_status
Definition raw_midi_parser.hpp:75
uint8_t pending_status
Definition raw_midi_parser.hpp:76
std::vector< uint8_t > sysex_buffer
Definition raw_midi_parser.hpp:73
DropReason
Definition raw_midi_parser.hpp:53
@ overflow
Definition raw_midi_parser.hpp:53
@ none
Definition raw_midi_parser.hpp:53
@ reset
Definition raw_midi_parser.hpp:53
@ aborted
Definition raw_midi_parser.hpp:53
void reserve_sysex(std::size_t worst_case_bytes)
Prepare internal SysEx storage for realtime use.
Definition raw_midi_parser.hpp:60