Sound Byte Libs 0.5.1-121-g3358a44
C++ firmware library for audio applications on 32-bit ARM Cortex-M processors
Loading...
Searching...
No Matches
parser.hpp
Go to the documentation of this file.
1// sbl/protocol/midi/parser.hpp — MIDI byte stream parser
2//
3// State machine that parses raw UART bytes into MidiEvent structs.
4// Supports running status, System Real-Time passthrough mid-message,
5// and Note On velocity 0 → Note Off conversion.
6//
7// SysEx is acknowledged (resets running status) but not buffered (V1).
8//
9// Inspired by MI stmlib/midi/midi.h (MIT license).
10//
11// Usage:
12// sbl::midi::Parser parser;
13// parser.set_callback(on_midi_event);
14// parser.set_channel(0); // Channel 1 (0-indexed), or 0xFF for omni
15//
16// // In main loop or UART RX ISR:
17// while (uart_available()) {
18// parser.push(uart_read_byte());
19// }
20
21#ifndef SBL_PROTOCOL_MIDI_PARSER_HPP_
22#define SBL_PROTOCOL_MIDI_PARSER_HPP_
23
24#include <cstdint>
25
27
28namespace sbl::midi {
29
30/**
31 * @brief MIDI byte stream parser — state machine with running status
32 *
33 * @note All public methods are ISR-safe — O(1) state machine with no
34 * hardware I/O. Callback is invoked synchronously from push().
35 */
36class Parser {
37public:
38 /**
39 * @brief Set the callback for parsed MIDI events
40 * @note ISR-safe — stores a function pointer, no I/O.
41 */
42 void set_callback(MidiCallback cb) { callback_ = cb; }
43
44 /**
45 * @brief Set channel filter
46 * @param ch Channel 0-15, or 0xFF for omni (receive all channels)
47 * @note ISR-safe — stores a byte, no I/O.
48 */
49 void set_channel(uint8_t ch) { channel_filter_ = ch; }
50
51 /**
52 * @brief Feed one byte from UART RX
53 *
54 * State machine processes the byte and dispatches complete messages
55 * via the callback. System Real-Time messages are dispatched
56 * immediately, even mid-message.
57 *
58 * @note ISR-safe — O(1) state machine, no blocking or I/O.
59 */
60 void push(uint8_t byte) {
61 // System Real-Time: dispatch immediately, don't disturb parser state
62 if (byte >= 0xF8) {
63 dispatch_realtime(byte);
64 return;
65 }
66
67 // Status byte?
68 if (byte & 0x80) {
69 // SysEx start — enter sysex mode, reset running status
70 if (byte == 0xF0) {
71 in_sysex_ = true;
72 running_status_ = 0;
73 data_count_ = 0;
74 return;
75 }
76
77 // SysEx end — exit sysex mode
78 if (byte == 0xF7) {
79 in_sysex_ = false;
80 return;
81 }
82
83 // Any other status byte ends SysEx
84 in_sysex_ = false;
85
86 // System Common (0xF1-0xF6) — clear running status
87 if (byte >= 0xF0) {
88 running_status_ = 0;
89 // System common messages: handle based on byte count
90 switch (byte) {
91 case 0xF1: // Time Code: 1 data byte
92 case 0xF3: // Song Select: 1 data byte
93 running_status_ = 0;
94 status_ = byte;
95 expected_ = 1;
96 data_count_ = 0;
97 return;
98 case 0xF2: // Song Position: 2 data bytes
99 running_status_ = 0;
100 status_ = byte;
101 expected_ = 2;
102 data_count_ = 0;
103 return;
104 case 0xF6: // Tune Request: no data
105 // Dispatch immediately
106 if (callback_) {
107 MidiEvent ev;
109 callback_(ev);
110 }
111 return;
112 default:
113 return;
114 }
115 }
116
117 // Channel message status byte
118 running_status_ = byte;
119 status_ = byte;
120 expected_ = expected_data_bytes(byte);
121 data_count_ = 0;
122 return;
123 }
124
125 // Data byte (0x00-0x7F)
126 if (in_sysex_) return; // Ignore SysEx data bytes
127
128 // No status context? Apply running status
129 if (data_count_ == 0 && running_status_ == 0) {
130 return; // Orphan data byte — discard
131 }
132
133 // If we're starting fresh data and have running status
134 if (data_count_ == 0 && status_ == 0) {
135 status_ = running_status_;
136 expected_ = expected_data_bytes(running_status_);
137 }
138
139 // Store data byte
140 if (data_count_ == 0) {
141 data1_ = byte;
142 } else {
143 data2_ = byte;
144 }
145 ++data_count_;
146
147 // Complete message?
148 if (data_count_ >= expected_) {
149 dispatch_channel_message();
150 data_count_ = 0;
151 status_ = 0; // Reset for running status
152 }
153 }
154
155 /**
156 * @brief Feed multiple bytes
157 * @note ISR-safe — O(n) loop over push(), no blocking or I/O.
158 */
159 void push(const uint8_t* data, uint16_t len) {
160 for (uint16_t i = 0; i < len; ++i) {
161 push(data[i]);
162 }
163 }
164
165private:
166 MidiCallback callback_ = nullptr;
167 uint8_t channel_filter_ = 0xFF; // Omni by default
168
169 // Parser state
170 uint8_t running_status_ = 0;
171 uint8_t status_ = 0;
172 uint8_t expected_ = 0;
173 uint8_t data_count_ = 0;
174 uint8_t data1_ = 0;
175 uint8_t data2_ = 0;
176 bool in_sysex_ = false;
177
178 void dispatch_realtime(uint8_t byte) {
179 if (!callback_) return;
180
181 MidiEvent ev;
182 switch (byte) {
183 case 0xF8: ev.type = MessageType::Clock; break;
184 case 0xFA: ev.type = MessageType::Start; break;
185 case 0xFB: ev.type = MessageType::Continue; break;
186 case 0xFC: ev.type = MessageType::Stop; break;
187 case 0xFE: ev.type = MessageType::ActiveSensing; break;
188 case 0xFF: ev.type = MessageType::SystemReset; break;
189 default: return;
190 }
191 callback_(ev);
192 }
193
194 void dispatch_channel_message() {
195 if (!callback_) return;
196
197 uint8_t msg_channel = status_ & 0x0F;
198
199 // Channel filter
200 if (channel_filter_ != 0xFF && msg_channel != channel_filter_) {
201 return;
202 }
203
204 MidiEvent ev;
205 ev.channel = msg_channel;
206 ev.data1 = data1_;
207 ev.data2 = data2_;
208
209 uint8_t msg_type = status_ & 0xF0;
210 switch (msg_type) {
211 case 0x80:
212 ev.type = MessageType::NoteOff;
213 break;
214 case 0x90:
215 // Note On with velocity 0 = Note Off
216 if (data2_ == 0) {
217 ev.type = MessageType::NoteOff;
218 } else {
219 ev.type = MessageType::NoteOn;
220 }
221 break;
222 case 0xA0: ev.type = MessageType::PolyPressure; break;
223 case 0xB0: ev.type = MessageType::ControlChange; break;
224 case 0xC0: ev.type = MessageType::ProgramChange; break;
225 case 0xD0: ev.type = MessageType::ChannelPressure; break;
226 case 0xE0: ev.type = MessageType::PitchBend; break;
227 default: return;
228 }
229
230 callback_(ev);
231 }
232};
233
234} // namespace sbl::midi
235
236#endif // SBL_PROTOCOL_MIDI_PARSER_HPP_
MIDI byte stream parser — state machine with running status.
Definition parser.hpp:36
void set_callback(MidiCallback cb)
Set the callback for parsed MIDI events.
Definition parser.hpp:42
void push(const uint8_t *data, uint16_t len)
Feed multiple bytes.
Definition parser.hpp:159
void push(uint8_t byte)
Feed one byte from UART RX.
Definition parser.hpp:60
void set_channel(uint8_t ch)
Set channel filter.
Definition parser.hpp:49
MIDI parser, encoder, SMF and sequencer.
Definition input.hpp:18
uint8_t expected_data_bytes(uint8_t status)
Number of data bytes expected for a channel message status byte.
Definition types.hpp:148
void(*)(const MidiEvent &event) MidiCallback
Callback type for parsed MIDI events.
Definition types.hpp:145
MIDI message types.
uint8_t channel
0-15 for channel messages, 0 for system
Definition types.hpp:42
MessageType type
Definition types.hpp:41