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
sequencer.hpp
Go to the documentation of this file.
1// sbl/protocol/midi/sequencer.hpp — Plays a Standard MIDI File into outputs (FDP-078)
2//
3// The sequencer owns a playhead over an SmfReader's tracks. It is told how
4// many samples have passed and never assumes a call rate (ADR-011): advance()
5// walks every track in tick order, converts ticks to time through the tempo
6// map, and hands each event that is due to a sink with the output it belongs
7// to. Events come out in the call where they fall due, so their timing is as
8// fine as the calls: to the sample if advance() is called per sample, to the
9// tick if it is called from a 1 kHz scheduler.
10//
11// Time is exact integer arithmetic. Tick → microseconds carries its remainder
12// across segments, and "due" compares microseconds × sample rate against
13// samples × 10^6, so nothing drifts over a long song or many loops.
14//
15// Routing. A format 1 file's track 0 is the conductor (the tempo map): its
16// tempo events apply, its channel events go nowhere unless mapped. Tracks
17// 1..N go to outputs 0..N-1. A format 0 file has one track, so it is split
18// by channel: channels 0..N-1 go to outputs 0..N-1. Either map can be
19// changed, and an output can rewrite every event's channel to one channel.
20//
21// Transport is standard MIDI:
22// start() play from the top — All Notes Off and every controller's first
23// value in the file sent to each output, then play. Each run from
24// the top begins the same way.
25// stop() stop where it is, All Notes Off; the playhead is kept.
26// resume() play on from the playhead (MIDI Continue).
27// With loop on, reaching the end sends All Notes Off and plays again from
28// the top; with loop off the sequencer stops and sends All Notes Off.
29//
30// Program Changes play on the first pass of a Start only. A program is the
31// voice's setup, not the song: a file names its sounds at tick 0, and a run
32// from the top should load them so every run begins the same — but a loop
33// wrap is the same run continuing, and a preset reloading on every wrap would
34// undo anything the player has dialled since (Michael, 2026-09-14).
35//
36// The SmfReader's bytes must outlive the sequencer.
37//
38// Usage:
39// void sink(void*, uint8_t output, const sbl::midi::MidiEvent& e) { ... }
40// sbl::midi::Sequencer<4> seq(sink, nullptr);
41// seq.load(smf);
42// seq.start();
43// seq.advance(elapsed_samples); // from the control tick
44
45#ifndef SBL_PROTOCOL_MIDI_SEQUENCER_HPP_
46#define SBL_PROTOCOL_MIDI_SEQUENCER_HPP_
47
48#include <cstdint>
49
52
53namespace sbl::midi {
54
55template<uint8_t Outputs = 4>
56class Sequencer {
57 static_assert(Outputs >= 1, "a sequencer needs an output");
58
59public:
60 static constexpr uint8_t OUTPUTS = Outputs;
61 static constexpr uint8_t MAX_TRACKS = SmfReader::MAX_TRACKS;
62 static constexpr uint8_t CHANNELS = 16;
63 static constexpr uint8_t NO_OUTPUT = 0xFF;
64 static constexpr uint8_t KEEP_CHANNEL = 0xFF;
65 static constexpr uint8_t MAX_SNAPSHOT = 64; ///< distinct (output, channel, controller) first values
66 static constexpr uint32_t DEFAULT_TEMPO_US = 500000; ///< 120 bpm: the SMF default
67 static constexpr uint32_t DEFAULT_SAMPLE_RATE = 48000;
68 static constexpr uint8_t ALL_NOTES_OFF = 123;
69
70 using Sink = void (*)(void* context, uint8_t output, const MidiEvent& event);
71
72 Sequencer(Sink sink, void* context) : sink_(sink), context_(context) {
73 for (uint8_t o = 0; o < Outputs; ++o) output_channel_[o] = KEEP_CHANNEL;
74 }
75
76 /// Natural units, before start(): the rate advance() counts in.
77 void set_sample_rate(uint32_t hz) { sample_rate_ = hz > 0 ? hz : DEFAULT_SAMPLE_RATE; }
78
79 /// Take a parsed file and apply the default map. Stops playback.
80 bool load(const SmfReader& smf) {
81 playing_ = false;
82 loaded_ = smf.error() == SmfError::None && smf.track_count() > 0;
83 if (!loaded_) return false;
84 format_ = smf.format();
85 track_count_ = smf.track_count();
86 ticks_per_quarter_ = smf.ticks_per_quarter();
87 for (uint8_t t = 0; t < MAX_TRACKS; ++t) {
88 tracks_[t] = t < track_count_ ? smf.track(t) : SmfTrack{};
89 track_output_[t] = (t >= 1 && t - 1 < Outputs) ? static_cast<uint8_t>(t - 1) : NO_OUTPUT;
90 }
91 for (uint8_t c = 0; c < CHANNELS; ++c) channel_output_[c] = c < Outputs ? c : NO_OUTPUT;
92 rewind();
93 scan();
94 finished_ = false;
95 return true;
96 }
97
98 /// Format 1: which output a track plays into (NO_OUTPUT for none).
99 void set_track_output(uint8_t track, uint8_t output) {
100 if (track >= MAX_TRACKS) return;
101 track_output_[track] = output < Outputs ? output : NO_OUTPUT;
102 if (loaded_) scan();
103 }
104
105 /// Format 0: which output a channel plays into (NO_OUTPUT for none).
106 void set_channel_output(uint8_t channel, uint8_t output) {
107 if (channel >= CHANNELS) return;
108 channel_output_[channel] = output < Outputs ? output : NO_OUTPUT;
109 if (loaded_) scan();
110 }
111
112 /// Send every event on an output on this channel (KEEP_CHANNEL to leave them).
113 void set_output_channel(uint8_t output, uint8_t channel) {
114 if (output >= Outputs) return;
115 output_channel_[output] = channel < CHANNELS ? channel : KEEP_CHANNEL;
116 if (loaded_) scan();
117 }
118
119 void set_loop(bool on) { loop_ = on; }
120 bool looping() const { return loop_; }
121
122 // ─── Transport (standard MIDI) ───────────────────────────────────
123
124 /// Start: play from the top with every output reset.
125 void start() {
126 if (!loaded_) return;
127 all_notes_off();
128 for (uint8_t i = 0; i < snapshot_count_; ++i) {
129 emit_cc(snapshot_[i].output, snapshot_[i].channel, snapshot_[i].controller, snapshot_[i].value);
130 }
131 rewind();
132 pass_start_scaled_ = now_scaled_;
133 pass_ = 0;
134 finished_ = false;
135 playing_ = true;
136 }
137
138 /// Stop: hold the playhead, silence the outputs.
139 void stop() {
140 if (!playing_) return;
141 playing_ = false;
142 all_notes_off();
143 }
144
145 /// Continue: play on from the playhead. Nothing to continue after the end.
146 void resume() {
147 if (loaded_ && !finished_) playing_ = true;
148 }
149
150 bool playing() const { return playing_; }
151 bool finished() const { return finished_; }
152 bool loaded() const { return loaded_; }
153
154 /// Where the playhead is in the song, in microseconds from the top.
155 uint64_t position_us() const { return song_us_; }
156
157 /**
158 * @brief Let `elapsed_samples` pass: emit every event that falls due
159 *
160 * Time counts while stopped too, so a resume picks up at the playhead
161 * without replaying the pause.
162 */
163 void advance(uint32_t elapsed_samples) {
164 const uint64_t step = static_cast<uint64_t>(elapsed_samples) * MICROS_PER_SECOND;
165 if (!loaded_ || !playing_) {
166 pass_start_scaled_ += step; // the song does not move while stopped
167 now_scaled_ += step;
168 return;
169 }
170 now_scaled_ += step;
171
172 for (;;) {
173 uint8_t best = NO_TRACK;
174 uint32_t best_tick = 0;
175 for (uint8_t t = 0; t < track_count_; ++t) {
176 const SmfEvent* ev = tracks_[t].peek();
177 if (ev != nullptr && (best == NO_TRACK || ev->tick < best_tick)) {
178 best = t;
179 best_tick = ev->tick;
180 }
181 }
182
183 if (best == NO_TRACK) { // every track has ended
184 if (!loop_ || song_us_ == 0) {
185 playing_ = false;
186 finished_ = true;
187 all_notes_off();
188 return;
189 }
190 all_notes_off();
191 pass_start_scaled_ += song_us_ * sample_rate_;
192 rewind();
193 ++pass_;
194 continue;
195 }
196
197 const uint64_t numerator =
198 static_cast<uint64_t>(best_tick - last_tick_) * tempo_us_ + tick_remainder_;
199 const uint64_t event_us = song_us_ + numerator / ticks_per_quarter_;
200 if (pass_start_scaled_ + event_us * sample_rate_ > now_scaled_) return; // not due yet
201
202 song_us_ = event_us;
203 tick_remainder_ = numerator % ticks_per_quarter_;
204 last_tick_ = best_tick;
205
206 const SmfEvent ev = *tracks_[best].peek();
207 tracks_[best].pop();
208 if (ev.kind == SmfEvent::Kind::Tempo && ev.tempo_us > 0) {
209 tempo_us_ = ev.tempo_us;
210 } else if (ev.kind == SmfEvent::Kind::Channel) {
211 route(best, ev.midi);
212 }
213 }
214 }
215
216private:
217 static constexpr uint64_t MICROS_PER_SECOND = 1000000;
218 static constexpr uint8_t NO_TRACK = 0xFF;
219
220 struct Controller {
221 uint8_t output;
222 uint8_t channel;
223 uint8_t controller;
224 uint8_t value;
225 };
226
227 void rewind() {
228 for (uint8_t t = 0; t < track_count_; ++t) tracks_[t].rewind();
229 tempo_us_ = DEFAULT_TEMPO_US;
230 song_us_ = 0;
231 last_tick_ = 0;
232 tick_remainder_ = 0;
233 }
234
235 /// The output an event plays into, and its channel after any rewrite.
236 bool destination(uint8_t track, const MidiEvent& e, uint8_t& output, uint8_t& channel) const {
237 output = format_ == 0 ? channel_output_[e.channel & (CHANNELS - 1)] : track_output_[track];
238 if (output >= Outputs) return false;
239 channel = output_channel_[output] == KEEP_CHANNEL ? e.channel : output_channel_[output];
240 return true;
241 }
242
243 void route(uint8_t track, MidiEvent e) {
244 if (e.type == MessageType::ProgramChange && pass_ > 0) return; // setup, not song
245 uint8_t output = 0;
246 uint8_t channel = 0;
247 if (!destination(track, e, output, channel)) return;
248 e.channel = channel;
249 if (sink_ != nullptr) sink_(context_, output, e);
250 }
251
252 void emit_cc(uint8_t output, uint8_t channel, uint8_t controller, uint8_t value) {
253 MidiEvent e;
255 e.channel = channel;
256 e.data1 = controller;
257 e.data2 = value;
258 if (sink_ != nullptr) sink_(context_, output, e);
259 }
260
261 void all_notes_off() {
262 for (uint8_t o = 0; o < Outputs; ++o) {
263 for (uint8_t c = 0; c < CHANNELS; ++c) {
264 if (used_channels_[o] & (1u << c)) emit_cc(o, c, ALL_NOTES_OFF, 0);
265 }
266 }
267 }
268
269 /// Which channels each output uses, and each controller's first value.
270 void scan() {
271 for (uint8_t o = 0; o < Outputs; ++o) used_channels_[o] = 0;
272 snapshot_count_ = 0;
273 for (uint8_t t = 0; t < track_count_; ++t) {
274 SmfTrack cursor = tracks_[t];
275 cursor.rewind();
276 while (const SmfEvent* ev = cursor.peek()) {
277 uint8_t output = 0;
278 uint8_t channel = 0;
279 if (ev->kind == SmfEvent::Kind::Channel && destination(t, ev->midi, output, channel)) {
280 used_channels_[output] |= static_cast<uint16_t>(1u << channel);
281 if (ev->midi.type == MessageType::ControlChange) remember(output, channel, ev->midi);
282 }
283 cursor.pop();
284 }
285 }
286 }
287
288 void remember(uint8_t output, uint8_t channel, const MidiEvent& e) {
289 for (uint8_t i = 0; i < snapshot_count_; ++i) {
290 const Controller& c = snapshot_[i];
291 if (c.output == output && c.channel == channel && c.controller == e.data1) return;
292 }
293 if (snapshot_count_ < MAX_SNAPSHOT) {
294 snapshot_[snapshot_count_++] = Controller{output, channel, e.data1, e.data2};
295 }
296 }
297
298 Sink sink_;
299 void* context_;
300
301 SmfTrack tracks_[MAX_TRACKS];
302 uint8_t track_output_[MAX_TRACKS] = {};
303 uint8_t channel_output_[CHANNELS] = {};
304 uint8_t output_channel_[Outputs] = {};
305 uint16_t used_channels_[Outputs] = {};
306 Controller snapshot_[MAX_SNAPSHOT] = {};
307 uint8_t snapshot_count_ = 0;
308
309 uint16_t format_ = 0;
310 uint8_t track_count_ = 0;
311 uint16_t ticks_per_quarter_ = 1;
312 uint32_t sample_rate_ = DEFAULT_SAMPLE_RATE;
313
314 bool loaded_ = false;
315 bool playing_ = false;
316 bool finished_ = false;
317 bool loop_ = false;
318
319 uint32_t tempo_us_ = DEFAULT_TEMPO_US;
320 uint64_t song_us_ = 0; ///< time of the last event reached, from the top
321 uint32_t last_tick_ = 0;
322 uint64_t tick_remainder_ = 0; ///< the part of a microsecond carried between segments
323 uint64_t now_scaled_ = 0; ///< samples elapsed × 10^6
324 uint64_t pass_start_scaled_ = 0; ///< where this pass of the song began, same units
325 uint32_t pass_ = 0; ///< 0 on the first pass after a Start; loop wraps count up
326};
327
328} // namespace sbl::midi
329
330#endif // SBL_PROTOCOL_MIDI_SEQUENCER_HPP_
void(*)(void *context, uint8_t output, const MidiEvent &event) Sink
Definition sequencer.hpp:70
bool loaded() const
static constexpr uint8_t ALL_NOTES_OFF
Definition sequencer.hpp:68
bool finished() const
void set_channel_output(uint8_t channel, uint8_t output)
Format 0: which output a channel plays into (NO_OUTPUT for none).
void set_sample_rate(uint32_t hz)
Natural units, before start(): the rate advance() counts in.
Definition sequencer.hpp:77
static constexpr uint8_t CHANNELS
Definition sequencer.hpp:62
static constexpr uint8_t MAX_TRACKS
Definition sequencer.hpp:61
uint64_t position_us() const
Where the playhead is in the song, in microseconds from the top.
static constexpr uint8_t NO_OUTPUT
Definition sequencer.hpp:63
void stop()
Stop: hold the playhead, silence the outputs.
bool load(const SmfReader &smf)
Take a parsed file and apply the default map. Stops playback.
Definition sequencer.hpp:80
void set_output_channel(uint8_t output, uint8_t channel)
Send every event on an output on this channel (KEEP_CHANNEL to leave them).
static constexpr uint8_t KEEP_CHANNEL
Definition sequencer.hpp:64
static constexpr uint32_t DEFAULT_SAMPLE_RATE
Definition sequencer.hpp:67
static constexpr uint8_t OUTPUTS
Definition sequencer.hpp:60
void start()
Start: play from the top with every output reset.
Sequencer(Sink sink, void *context)
Definition sequencer.hpp:72
void set_loop(bool on)
void set_track_output(uint8_t track, uint8_t output)
Format 1: which output a track plays into (NO_OUTPUT for none).
Definition sequencer.hpp:99
void resume()
Continue: play on from the playhead. Nothing to continue after the end.
bool playing() const
static constexpr uint32_t DEFAULT_TEMPO_US
120 bpm: the SMF default
Definition sequencer.hpp:66
void advance(uint32_t elapsed_samples)
Let elapsed_samples pass: emit every event that falls due.
static constexpr uint8_t MAX_SNAPSHOT
distinct (output, channel, controller) first values
Definition sequencer.hpp:65
bool looping() const
uint8_t track_count() const
static constexpr uint8_t MAX_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.
const SmfEvent * peek()
The next event, or nullptr when the track has ended.
MIDI parser, encoder, SMF and sequencer.
Definition input.hpp:18
MIDI message types.
Standard MIDI File reader (FDP-078)
One event from a track, with its absolute tick.
uint32_t tempo_us
Tempo: microseconds per quarter note.
uint32_t tick
ticks from the start of the track
MidiEvent midi
Channel.