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
smf_reader.hpp
Go to the documentation of this file.
1// sbl/protocol/midi/smf_reader.hpp — Standard MIDI File reader (FDP-078)
2//
3// Reads a Standard MIDI File already in memory: no filesystem, no allocation,
4// nothing copied. The reader checks the header and finds the track chunks;
5// an SmfTrack walks one track's events in order, one at a time.
6//
7// What it understands (Standard MIDI Files 1.0, shelf `smf-1-0`):
8// - formats 0 and 1 (format 2 is refused)
9// - ticks-per-quarter-note division (SMPTE division is refused)
10// - variable-length delta times
11// - running status, which carries across delta times and is cancelled by
12// SysEx and meta events (p. 5, p. 7)
13// - the meta events a player needs: Set Tempo, Time Signature, Track Name
14// and End of Track (p. 8); other meta events and SysEx are skipped
15// - chunks that are not MTrk, which are skipped
16//
17// Usage:
18// sbl::midi::SmfReader smf;
19// if (smf.parse(bytes, size) != sbl::midi::SmfError::None) { ... }
20// sbl::midi::SmfTrack track = smf.track(1);
21// while (const sbl::midi::SmfEvent* ev = track.peek()) { ...; track.pop(); }
22
23#ifndef SBL_PROTOCOL_MIDI_SMF_READER_HPP_
24#define SBL_PROTOCOL_MIDI_SMF_READER_HPP_
25
26#include <cstddef>
27#include <cstdint>
28
30
31namespace sbl::midi {
32
33enum class SmfError : uint8_t {
34 None,
35 NotSmf, ///< no MThd header chunk at the start
36 BadHeader, ///< header too short, zero division, or format 0 with more than one track
37 UnsupportedFormat, ///< format 2
38 SmpteDivision, ///< division in SMPTE frames rather than ticks per quarter note
39 TooManyTracks, ///< more tracks than SmfReader::MAX_TRACKS
40 Truncated, ///< a chunk runs past the end of the data, or tracks are missing
41};
42
43/// One event from a track, with its absolute tick.
44struct SmfEvent {
45 enum class Kind : uint8_t { Channel, Tempo, TimeSignature, TrackName, EndOfTrack };
46
48 uint32_t tick = 0; ///< ticks from the start of the track
49 MidiEvent midi{}; ///< Channel
50 uint32_t tempo_us = 0; ///< Tempo: microseconds per quarter note
51 uint8_t numerator = 0; ///< TimeSignature
52 uint8_t denominator_power = 0; ///< TimeSignature: the denominator is 2^power
53 const uint8_t* text = nullptr; ///< TrackName: points into the file, not terminated
54 uint32_t text_length = 0;
55};
56
57/**
58 * @brief A cursor over one track chunk
59 *
60 * peek() decodes the next event without consuming it; pop() consumes it. A
61 * malformed event ends the track and sets error(). The file's bytes must
62 * outlive the cursor.
63 */
64class SmfTrack {
65public:
66 SmfTrack() = default;
67 SmfTrack(const uint8_t* data, uint32_t length) : data_(data), length_(length) { rewind(); }
68
69 void rewind() {
70 pos_ = 0;
71 tick_ = 0;
72 running_status_ = 0;
73 has_pending_ = false;
74 error_ = false;
75 done_ = (data_ == nullptr);
76 }
77
78 /// The next event, or nullptr when the track has ended.
79 const SmfEvent* peek() {
80 if (!has_pending_ && !done_) has_pending_ = decode(pending_);
81 return has_pending_ ? &pending_ : nullptr;
82 }
83
84 void pop() { has_pending_ = false; }
85
86 bool done() const { return done_ && !has_pending_; }
87 bool error() const { return error_; }
88
89private:
90 static constexpr uint8_t STATUS_BIT = 0x80;
91 static constexpr uint8_t META = 0xFF;
92 static constexpr uint8_t SYSEX = 0xF0;
93 static constexpr uint8_t SYSEX_ESCAPE = 0xF7;
94 static constexpr uint8_t SYSTEM_FIRST = 0xF0;
95 static constexpr uint8_t META_TRACK_NAME = 0x03;
96 static constexpr uint8_t META_END_OF_TRACK = 0x2F;
97 static constexpr uint8_t META_TEMPO = 0x51;
98 static constexpr uint8_t META_TIME_SIGNATURE = 0x58;
99 static constexpr uint32_t TEMPO_LENGTH = 3;
100 static constexpr uint32_t TIME_SIGNATURE_LENGTH = 2; ///< nn dd; the clock bytes are not used
101 static constexpr uint8_t VLQ_MAX_BYTES = 4;
102 static constexpr uint8_t VLQ_MORE = 0x80;
103 static constexpr uint8_t VLQ_BITS = 0x7F;
104 static constexpr uint8_t VLQ_SHIFT = 7;
105 static constexpr uint8_t STATUS_KIND_MASK = 0xF0;
106 static constexpr uint8_t CHANNEL_MASK = 0x0F;
107 static constexpr uint8_t PROGRAM_CHANGE = 0xC0;
108 static constexpr uint8_t CHANNEL_PRESSURE = 0xD0;
109
110 bool fail() {
111 error_ = true;
112 done_ = true;
113 return false;
114 }
115
116 bool read_vlq(uint32_t& out) {
117 out = 0;
118 for (uint8_t i = 0; i < VLQ_MAX_BYTES; ++i) {
119 if (pos_ >= length_) return false;
120 const uint8_t b = data_[pos_++];
121 out = (out << VLQ_SHIFT) | (b & VLQ_BITS);
122 if ((b & VLQ_MORE) == 0) return true;
123 }
124 return false; // longer than four bytes: not a valid delta or length
125 }
126
127 /// Decode the next event a player cares about; skip the rest.
128 bool decode(SmfEvent& ev) {
129 while (pos_ < length_) {
130 uint32_t delta = 0;
131 if (!read_vlq(delta)) return fail();
132 tick_ += delta;
133 if (pos_ >= length_) return fail();
134 const uint8_t lead = data_[pos_];
135
136 if (lead == META) {
137 if (pos_ + 2 > length_) return fail();
138 const uint8_t type = data_[pos_ + 1];
139 pos_ += 2;
140 uint32_t len = 0;
141 if (!read_vlq(len) || pos_ + len > length_) return fail();
142 const uint32_t body = pos_;
143 pos_ += len;
144 running_status_ = 0; // meta events cancel running status
145
146 ev = SmfEvent{};
147 ev.tick = tick_;
148 switch (type) {
149 case META_END_OF_TRACK:
151 done_ = true;
152 return true;
153 case META_TEMPO:
154 if (len < TEMPO_LENGTH) return fail();
155 ev.kind = SmfEvent::Kind::Tempo;
156 ev.tempo_us = (static_cast<uint32_t>(data_[body]) << 16) |
157 (static_cast<uint32_t>(data_[body + 1]) << 8) |
158 data_[body + 2];
159 return true;
160 case META_TIME_SIGNATURE:
161 if (len < TIME_SIGNATURE_LENGTH) return fail();
163 ev.numerator = data_[body];
164 ev.denominator_power = data_[body + 1];
165 return true;
166 case META_TRACK_NAME:
168 ev.text = data_ + body;
169 ev.text_length = len;
170 return true;
171 default:
172 continue; // a meta event a player doesn't need
173 }
174 }
175
176 if (lead == SYSEX || lead == SYSEX_ESCAPE) {
177 ++pos_;
178 uint32_t len = 0;
179 if (!read_vlq(len) || pos_ + len > length_) return fail();
180 pos_ += len;
181 running_status_ = 0; // SysEx cancels running status too
182 continue;
183 }
184
185 uint8_t status = running_status_;
186 if (lead & STATUS_BIT) {
187 if (lead >= SYSTEM_FIRST) return fail(); // no other system messages in a track
188 status = lead;
189 running_status_ = lead;
190 ++pos_;
191 } else if (status == 0) {
192 return fail(); // data with no status in effect
193 }
194
195 const uint8_t kind = status & STATUS_KIND_MASK;
196 const uint8_t data_bytes = (kind == PROGRAM_CHANGE || kind == CHANNEL_PRESSURE) ? 1 : 2;
197 if (pos_ + data_bytes > length_) return fail();
198
199 ev = SmfEvent{};
200 ev.kind = SmfEvent::Kind::Channel;
201 ev.tick = tick_;
202 ev.midi.type = static_cast<MessageType>(kind);
203 ev.midi.channel = status & CHANNEL_MASK;
204 ev.midi.data1 = data_[pos_];
205 ev.midi.data2 = data_bytes == 2 ? data_[pos_ + 1] : 0;
206 pos_ += data_bytes;
207 return true;
208 }
209 done_ = true; // ran out without End of Track: accept the end
210 return false;
211 }
212
213 const uint8_t* data_ = nullptr;
214 uint32_t length_ = 0;
215 uint32_t pos_ = 0;
216 uint32_t tick_ = 0;
217 uint8_t running_status_ = 0;
218 bool has_pending_ = false;
219 bool done_ = true;
220 bool error_ = false;
221 SmfEvent pending_{};
222};
223
225public:
226 static constexpr uint8_t MAX_TRACKS = 16;
227
228 /// Check the header and find the tracks. The bytes must outlive the reader and its tracks.
229 SmfError parse(const uint8_t* data, size_t size) {
230 *this = SmfReader{};
231 if (data == nullptr || size < CHUNK_HEADER || !id_is(data, "MThd")) return fail(SmfError::NotSmf);
232 const uint32_t header_length = be32(data + ID_LENGTH);
233 if (header_length < HEADER_BODY || size < CHUNK_HEADER + header_length) return fail(SmfError::BadHeader);
234
235 const uint16_t format = be16(data + CHUNK_HEADER);
236 const uint16_t tracks = be16(data + CHUNK_HEADER + 2);
237 const uint16_t division = be16(data + CHUNK_HEADER + 4);
238 if (division & SMPTE_DIVISION_BIT) return fail(SmfError::SmpteDivision);
239 if (division == 0) return fail(SmfError::BadHeader);
240 if (format > 1) return fail(SmfError::UnsupportedFormat);
241 if (format == 0 && tracks != 1) return fail(SmfError::BadHeader);
242 if (tracks > MAX_TRACKS) return fail(SmfError::TooManyTracks);
243
244 size_t pos = CHUNK_HEADER + header_length;
245 uint8_t found = 0;
246 while (found < tracks) {
247 if (pos + CHUNK_HEADER > size) return fail(SmfError::Truncated);
248 const uint32_t length = be32(data + pos + ID_LENGTH);
249 if (pos + CHUNK_HEADER + length > size) return fail(SmfError::Truncated);
250 if (id_is(data + pos, "MTrk")) {
251 track_data_[found] = data + pos + CHUNK_HEADER;
252 track_length_[found] = length;
253 ++found;
254 } // any other chunk: skipped, as the spec asks
255 pos += CHUNK_HEADER + length;
256 }
257
258 format_ = format;
259 track_count_ = found;
260 ticks_per_quarter_ = division;
261 return SmfError::None;
262 }
263
264 SmfError error() const { return error_; }
265 uint16_t format() const { return format_; }
266 uint8_t track_count() const { return track_count_; }
267 uint16_t ticks_per_quarter() const { return ticks_per_quarter_; }
268
269 /// A fresh cursor at the start of track `index`; an empty one if out of range.
270 SmfTrack track(uint8_t index) const {
271 if (index >= track_count_) return SmfTrack{};
272 return SmfTrack{track_data_[index], track_length_[index]};
273 }
274
275private:
276 static constexpr size_t ID_LENGTH = 4;
277 static constexpr size_t CHUNK_HEADER = 8; ///< id + 32-bit length
278 static constexpr uint32_t HEADER_BODY = 6; ///< format, ntrks, division
279 static constexpr uint16_t SMPTE_DIVISION_BIT = 0x8000;
280
281 static bool id_is(const uint8_t* p, const char* id) {
282 for (size_t i = 0; i < ID_LENGTH; ++i) {
283 if (p[i] != static_cast<uint8_t>(id[i])) return false;
284 }
285 return true;
286 }
287 static uint16_t be16(const uint8_t* p) { return static_cast<uint16_t>((p[0] << 8) | p[1]); }
288 static uint32_t be32(const uint8_t* p) {
289 return (static_cast<uint32_t>(p[0]) << 24) | (static_cast<uint32_t>(p[1]) << 16) |
290 (static_cast<uint32_t>(p[2]) << 8) | p[3];
291 }
292
293 SmfError fail(SmfError e) {
294 error_ = e;
295 return e;
296 }
297
298 const uint8_t* track_data_[MAX_TRACKS] = {};
299 uint32_t track_length_[MAX_TRACKS] = {};
300 SmfError error_ = SmfError::None;
301 uint16_t format_ = 0;
302 uint8_t track_count_ = 0;
303 uint16_t ticks_per_quarter_ = 0;
304};
305
306} // namespace sbl::midi
307
308#endif // SBL_PROTOCOL_MIDI_SMF_READER_HPP_
uint8_t track_count() const
static constexpr uint8_t MAX_TRACKS
SmfError parse(const uint8_t *data, size_t size)
Check the header and find the tracks. The bytes must outlive the reader and its tracks.
uint16_t format() const
SmfError error() const
SmfTrack track(uint8_t index) const
A fresh cursor at the start of track index; an empty one if out of range.
uint16_t ticks_per_quarter() const
A cursor over one track chunk.
bool error() const
SmfTrack(const uint8_t *data, uint32_t length)
bool done() const
const SmfEvent * peek()
The next event, or nullptr when the track has ended.
MIDI parser, encoder, SMF and sequencer.
Definition input.hpp:18
@ NotSmf
no MThd header chunk at the start
@ TooManyTracks
more tracks than SmfReader::MAX_TRACKS
@ Truncated
a chunk runs past the end of the data, or tracks are missing
@ BadHeader
header too short, zero division, or format 0 with more than one track
@ UnsupportedFormat
format 2
@ SmpteDivision
division in SMPTE frames rather than ticks per quarter note
MIDI message types.
One event from a track, with its absolute tick.
uint32_t tempo_us
Tempo: microseconds per quarter note.
const uint8_t * text
TrackName: points into the file, not terminated.
uint32_t tick
ticks from the start of the track
uint8_t denominator_power
TimeSignature: the denominator is 2^power.
MidiEvent midi
Channel.
uint8_t numerator
TimeSignature.