Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
sysex_accumulator.hpp
Go to the documentation of this file.
1#pragma once
2
3// SysExAccumulator — shared MIDI SysEx aggregation state machine.
4//
5// MIDI SysEx messages (F0 ... F7) can span multiple transport packets:
6// Android's MIDI API delivers raw bytes whenever they arrive, BLE MIDI
7// fragments into ~20-byte chunks, USB-MIDI sends 4-byte event packets,
8// etc. None of those transports guarantee a full sysex arrives in one
9// delivery. This accumulator buffers until F7 or an abort, then emits
10// the complete payload.
11//
12// State machine:
13//
14// Idle --[F0]--> Buffering
15// Buffering --[data]-> Buffering (append)
16// Buffering --[F7]--> emit complete, back to Idle
17// Buffering --[status != F7, != realtime]--> emit aborted, retry with status
18// Any --[realtime (F8-FF except F7)]--> pass-through (realtime bytes
19// can interrupt sysex without terminating it per
20// the MIDI 1.0 spec — they're delivered outside
21// the sysex stream).
22//
23// The accumulator is not thread-safe; wrap it per-port. Android's MIDI
24// receiver is single-threaded per device, so the per-port model fits.
25//
26// RT-safety contract:
27//
28// - Call reserve(N) before entering the audio/MIDI callback, where N is at
29// least the largest F0...F7 payload the port will accept.
30// - After that preparation, feed(), reset(), in_progress(), and
31// partial_size() do not allocate while messages stay within the reserved
32// size.
33// - The emit callback and its destination storage are caller-owned. Keep
34// the SysexEmitFn object and any payload consumer preconstructed and
35// allocation-free when using this accumulator from realtime code.
36//
37// Unit tests live in test/test_sysex_accumulator.cpp — see that file
38// for coverage of the edge cases referenced in the state machine above.
39
40#include <cstdint>
41#include <functional>
42#include <vector>
43
44namespace pulp::midi {
45
52 std::function<void(const std::vector<std::uint8_t>& payload, bool complete)>;
53
55public:
58 void reserve(std::size_t worst_case_bytes) {
59 buffer_.reserve(worst_case_bytes);
60 }
61
83
84 Classification feed(std::uint8_t byte, const SysexEmitFn& emit) {
85 // Realtime messages F8-FE are delivered asynchronously and
86 // don't participate in sysex framing. Pass them through.
87 if (is_realtime(byte)) return Classification::passthrough;
88
89 if (!in_sysex_) {
90 if (byte == 0xF0) {
91 in_sysex_ = true;
92 buffer_.clear();
93 buffer_.push_back(0xF0);
95 }
96 // Regular short-message byte — caller handles it.
98 }
99
100 // In-sysex branch.
101 if (byte == 0xF7) {
102 buffer_.push_back(0xF7);
103 emit(buffer_, /*complete=*/true);
104 in_sysex_ = false;
105 buffer_.clear();
107 }
108
109 if (is_status(byte)) {
110 // New status interrupted an unterminated F0. Flush the
111 // partial payload (without F7) and return without consuming
112 // the aborter — caller re-feeds it as a fresh status.
113 emit(buffer_, /*complete=*/false);
114 in_sysex_ = false;
115 buffer_.clear();
117 }
118
119 // Normal data byte inside an active sysex.
120 buffer_.push_back(byte);
122 }
123
126 void feed(const std::uint8_t* data, std::size_t count,
127 const SysexEmitFn& emit) {
128 for (std::size_t i = 0; i < count; ++i) {
129 auto cls = feed(data[i], emit);
130 if (cls == Classification::aborted) {
131 // Re-feed the aborter byte as a fresh status.
132 feed(data[i], emit);
133 }
134 }
135 }
136
138 bool in_progress() const noexcept { return in_sysex_; }
139
142 void reset() noexcept {
143 in_sysex_ = false;
144 buffer_.clear();
145 }
146
149 std::size_t partial_size() const noexcept { return buffer_.size(); }
150
151private:
152 static bool is_status(std::uint8_t b) noexcept { return (b & 0x80) != 0; }
153 static bool is_realtime(std::uint8_t b) noexcept {
154 // MIDI 1.0 System Realtime Messages occupy 0xF8-0xFF, minus
155 // 0xF7 (EOX, handled explicitly in feed()). 0xF9 and 0xFD are
156 // officially undefined/reserved by MIDI 1.0, but the contract
157 // documented at the top of this file — and the behavior
158 // required by transports that emit them — is that everything
159 // in 0xF8-0xFF *except F7* passes through without disturbing
160 // the SysEx state machine. Treating 0xF9/0xFD as generic
161 // status bytes aborted otherwise-valid SysEx on devices that
162 // forward the reserved codes.
163 //
164 // Pass-through is safe because the accumulator only classifies
165 // the byte — the caller's normal short-message path decides
166 // whether to ignore, log, or forward the reserved code.
167 return b >= 0xF8 && b <= 0xFF;
168 }
169
170 bool in_sysex_ = false;
171 std::vector<std::uint8_t> buffer_;
172};
173
174} // namespace pulp::midi
Definition sysex_accumulator.hpp:54
void feed(const std::uint8_t *data, std::size_t count, const SysexEmitFn &emit)
Feed a run of bytes — convenience over a delivery boundary.
Definition sysex_accumulator.hpp:126
std::size_t partial_size() const noexcept
Size of the in-progress payload (0 when idle).
Definition sysex_accumulator.hpp:149
void reserve(std::size_t worst_case_bytes)
Prepare internal payload storage for realtime use.
Definition sysex_accumulator.hpp:58
bool in_progress() const noexcept
True when buffer_ holds an open F0 without a matching F7.
Definition sysex_accumulator.hpp:138
Classification
Feed a single byte through the state machine.
Definition sysex_accumulator.hpp:77
@ passthrough
Definition sysex_accumulator.hpp:81
@ completed
Definition sysex_accumulator.hpp:79
@ in_sysex
Definition sysex_accumulator.hpp:78
@ aborted
Definition sysex_accumulator.hpp:80
void reset() noexcept
Clear any accumulated partial state.
Definition sysex_accumulator.hpp:142
Classification feed(std::uint8_t byte, const SysexEmitFn &emit)
Definition sysex_accumulator.hpp:84
Definition custom_node_events.hpp:26
std::function< void(const std::vector< std::uint8_t > &payload, bool complete)> SysexEmitFn
Callback shape: receives the complete sysex payload (including the leading 0xF0 and trailing 0xF7) pl...
Definition sysex_accumulator.hpp:51