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