Loading...
Searching...
No Matches
MIDIClockEngine.hpp
1#pragma once
2
3/* SPDX-License-Identifier: GPL-3.0-or-later */
4
5#include <algorithm>
6#include <array>
7#include <cmath>
8#include <cstdint>
9#include <limits>
10
18{
20inline constexpr double clock_rate_factor(int index) noexcept
21{
22 constexpr double factors[] = {1., 0.25, 1. / 3., 0.5, 2., 3., 4.};
23 return (index >= 0 && index < int(std::size(factors))) ? factors[index] : 1.;
24}
25
27inline constexpr double nudge_factor(int nudge) noexcept
28{
29 return nudge > 0 ? 1.1 : nudge < 0 ? 0.9 : 1.;
30}
31
32inline constexpr double min_tempo = 20.;
33inline constexpr double max_tempo = 300.;
34
40{
41 static constexpr int max_taps = 32;
42 static constexpr int64_t timeout_ns = 3'000'000'000;
43
44 void tap(int64_t now) noexcept
45 {
46 if(m_count > 0)
47 {
48 const int64_t since = now - last();
49 if(since > timeout_ns || since <= 0)
50 m_count = 0;
51 }
52
53 m_head = (m_head + 1) % max_taps;
54 m_taps[m_head] = now;
55 m_count = std::min(m_count + 1, max_taps);
56 }
57
58 void reset() noexcept { m_count = 0; }
59
60 int count() const noexcept { return m_count; }
61 int64_t last() const noexcept { return m_taps[m_head]; }
62
64 double interval_ns() const noexcept
65 {
66 if(m_count < 2)
67 return 0.;
68 const int first = (m_head - (m_count - 1) + max_taps) % max_taps;
69 return double(m_taps[m_head] - m_taps[first]) / double(m_count - 1);
70 }
71
73 double tempo() const noexcept
74 {
75 const double iv = interval_ns();
76 return iv > 0. ? std::clamp(60e9 / iv, min_tempo, max_tempo) : 0.;
77 }
78
79private:
80 std::array<int64_t, max_taps> m_taps{};
81 int m_head{max_taps - 1};
82 int m_count{};
83};
84
85struct message
86{
88 int64_t at{};
90 std::array<uint8_t, 10> bytes{};
91 int size{};
92};
93
94enum class transport_command : int
95{
96 None,
97 Start,
98 Stop,
99 Continue
100};
101
103{
105 double bpm = 120.;
106 double rate = 1.;
107 int nudge = 0;
108 double offset_ms = 0.;
109 bool send_clock = true;
110 bool send_transport = true;
111};
112
132{
133 static constexpr int count_in_taps = 4;
134 static constexpr int64_t never = std::numeric_limits<int64_t>::max();
135
137 double tempo(const free_clock_params& p) const noexcept
138 {
139 if(m_tapped_tempo > 0.)
140 return m_tapped_tempo;
141 return std::clamp(p.bpm, min_tempo, max_tempo);
142 }
143
144 int64_t period_ns(const free_clock_params& p) const noexcept
145 {
146 const double rate = p.rate > 0. ? p.rate : 1.;
147 return int64_t(60e9 / (tempo(p) * nudge_factor(p.nudge) * 24. * rate));
148 }
149
150 bool running() const noexcept { return m_state == state::running; }
151 bool starting() const noexcept { return m_state == state::starting; }
152 bool count_in_pending() const noexcept { return m_starting_from_taps; }
153
155 int64_t song_position() const noexcept { return m_clocks / 6; }
156
157 const tap_tempo& taps() const noexcept { return m_taps; }
158
160 int64_t next_deadline(const free_clock_params& p) const noexcept
161 {
162 switch(m_state)
163 {
164 case state::starting:
165 return m_start_at;
166 case state::running:
167 return m_last_tick + period_ns(p);
168 default:
169 return never;
170 }
171 }
172
173 template <typename Send>
174 void command(transport_command c, int64_t now, const free_clock_params& p, Send&& send)
175 {
176 update_params(p);
177 switch(c)
178 {
179 case transport_command::None:
180 break;
181 case transport_command::Start:
182 m_state = state::starting;
183 m_start_is_continue = false;
184 m_starting_from_taps = false;
185 m_start_at = now + delay_ns(p);
186 break;
187 case transport_command::Continue:
188 if(m_state == state::stopped)
189 {
190 m_state = state::starting;
191 m_start_is_continue = true;
192 m_starting_from_taps = false;
193 m_start_at = now + delay_ns(p);
194 }
195 break;
196 case transport_command::Stop:
197 if(m_state != state::stopped)
198 {
199 m_state = state::stopped;
200 m_starting_from_taps = false;
201 if(p.send_transport)
202 send(message{now, {0xFC}, 1});
203 }
204 m_taps.reset();
205 break;
206 }
207 }
208
209 void tap(int64_t now, const free_clock_params& p) noexcept
210 {
211 update_params(p);
212 m_taps.tap(now);
213 if(m_taps.count() >= 2)
214 m_tapped_tempo = m_taps.tempo();
215
216 const bool idle
217 = m_state == state::stopped || (m_state == state::starting && m_starting_from_taps);
218 if(idle && m_taps.count() >= count_in_taps)
219 {
220 m_state = state::starting;
221 m_start_is_continue = false;
222 m_starting_from_taps = true;
223 m_start_at = m_taps.last() + int64_t(m_taps.interval_ns()) + delay_ns(p);
224 }
225 }
226
228 template <typename Send>
229 void process(int64_t now, const free_clock_params& p, Send&& send)
230 {
231 update_params(p);
232
233 if(m_state == state::starting && now >= m_start_at)
234 {
235 const int64_t t = m_start_at;
236 // The receiver resumes from a Song Position, a 16th note: on the next
237 // one, so that its count of clocks and ours agree.
238 m_clocks = m_start_is_continue ? (m_clocks + 5) / 6 * 6 : 0;
239 if(p.send_transport)
240 {
241 const int64_t pos = std::clamp<int64_t>(song_position(), 0, 16383);
242 send(message{t, {0xF2, uint8_t(pos & 0x7F), uint8_t((pos >> 7) & 0x7F)}, 3});
243 send(message{t, {uint8_t(m_start_is_continue ? 0xFB : 0xFA)}, 1});
244 }
245 if(p.send_clock)
246 send(message{t, {0xF8}, 1});
247 m_clocks++;
248 m_last_tick = t;
249 m_state = state::running;
250 m_starting_from_taps = false;
251 }
252
253 if(m_state == state::running)
254 {
255 for(int64_t next = m_last_tick + period_ns(p); next <= now;
256 next = m_last_tick + period_ns(p))
257 {
258 if(p.send_clock)
259 send(message{next, {0xF8}, 1});
260 m_clocks++;
261 m_last_tick = next;
262 }
263 }
264 }
265
266private:
267 enum class state
268 {
269 stopped,
270 starting,
271 running
272 };
273
274 static int64_t delay_ns(const free_clock_params& p) noexcept
275 {
276 return p.offset_ms > 0. ? int64_t(p.offset_ms * 1e6) : 0;
277 }
278
279 void update_params(const free_clock_params& p) noexcept
280 {
281 if(p.bpm != m_bpm_control)
282 {
283 if(m_has_bpm_control)
284 m_tapped_tempo = 0.;
285 m_bpm_control = p.bpm;
286 m_has_bpm_control = true;
287 }
288
289 const int64_t delay = delay_ns(p);
290 if(delay != m_delay)
291 {
292 const int64_t delta = delay - m_delay;
293 m_delay = delay;
294 if(m_state == state::starting)
295 m_start_at += delta;
296 else if(m_state == state::running)
297 m_last_tick += delta;
298 }
299 }
300
301 tap_tempo m_taps;
302 double m_tapped_tempo{};
303 double m_bpm_control{};
304 bool m_has_bpm_control{};
305 int64_t m_delay{};
306
307 state m_state{state::stopped};
308 bool m_start_is_continue{};
309 bool m_starting_from_taps{};
310 int64_t m_start_at{};
311 int64_t m_last_tick{};
312 int64_t m_clocks{};
313};
314
334{
335 double last_clock_time = 0.0;
336 uint32_t clock_tick_count = 0;
337 double previous_offset = 0.0;
339 bool clock_at_locate = false;
340
351 int64_t locate(double time, double quarters, double tempo, double rate) noexcept
352 {
353 const double sixteenths_per_quarter = 4.0 * rate;
354 const double position = std::ceil(quarters * sixteenths_per_quarter - 1e-9);
355 const double ahead = position / sixteenths_per_quarter - quarters;
356 last_clock_time = time + ahead * 60.0 / tempo;
357 previous_offset = 0.0;
358 clock_at_locate = true;
359 return int64_t(position);
360 }
361
363 template <typename Emit>
365 double current_time, double frame_duration, int64_t frames, double tempo,
366 double factor, double offset, Emit&& emit)
367 {
368 const double seconds_per_tick = 60.0 / (tempo * 24.0 * factor);
369
370 const double buffer_start_time = current_time - offset;
371 const double buffer_end_time = buffer_start_time + frame_duration;
372 double gap_start_time = buffer_start_time;
373 if(frames > 0)
374 {
375 if(offset < previous_offset)
376 gap_start_time -= previous_offset - offset;
377 previous_offset = offset;
378 }
379
380 double next_tick_time = last_clock_time + seconds_per_tick;
381 // Not before the gap: the clock of the located point is never skipped
382 if(clock_at_locate && frames > 0)
383 next_tick_time = std::max(last_clock_time, gap_start_time);
384
385 while(next_tick_time < buffer_end_time)
386 {
387 clock_at_locate = false;
388 if(next_tick_time >= buffer_start_time)
389 {
390 int64_t sample_offset
391 = ((next_tick_time - buffer_start_time) / frame_duration) * frames;
392 sample_offset = std::clamp<int64_t>(sample_offset, 0, frames - 1);
393
394 emit(sample_offset);
395 clock_tick_count++;
396 }
397 else if(next_tick_time >= gap_start_time)
398 {
399 emit(int64_t(0));
400 clock_tick_count++;
401 }
402
403 last_clock_time = next_tick_time;
404 next_tick_time += seconds_per_tick;
405 }
406 }
407};
408}
Definition MIDIClockEngine.hpp:18
constexpr double nudge_factor(int nudge) noexcept
Tempo factor while a nudge button is held: -1 is down, +1 is up.
Definition MIDIClockEngine.hpp:27
constexpr double clock_rate_factor(int index) noexcept
Clock rate factors, in the order of the "Clock rate" control's values.
Definition MIDIClockEngine.hpp:20
Definition MIDIClockEngine.hpp:103
double bpm
The BPM control. A tapped tempo replaces it until it changes.
Definition MIDIClockEngine.hpp:105
Definition MIDIClockEngine.hpp:132
void process(int64_t now, const free_clock_params &p, Send &&send)
Sends what is due at or before now.
Definition MIDIClockEngine.hpp:229
int64_t next_deadline(const free_clock_params &p) const noexcept
When process() next has something to send.
Definition MIDIClockEngine.hpp:160
int64_t song_position() const noexcept
Position in MIDI beats (16th notes) of the receiver: 6 clocks each.
Definition MIDIClockEngine.hpp:155
double tempo(const free_clock_params &p) const noexcept
The tempo before nudge and rate: the tapped one, or the BPM control.
Definition MIDIClockEngine.hpp:137
Definition MIDIClockEngine.hpp:86
int64_t at
Time at which the message is due, in the caller's clock.
Definition MIDIClockEngine.hpp:88
std::array< uint8_t, 10 > bytes
Large enough for a full MTC frame.
Definition MIDIClockEngine.hpp:90
Definition MIDIClockEngine.hpp:40
double tempo() const noexcept
Tempo of the series in BPM, 0 with less than 2 taps.
Definition MIDIClockEngine.hpp:73
double interval_ns() const noexcept
Average interval between the taps of the series, 0 with less than 2 taps.
Definition MIDIClockEngine.hpp:64
Definition MIDIClockEngine.hpp:334
bool clock_at_locate
The clock at last_clock_time is still to send.
Definition MIDIClockEngine.hpp:339
int64_t locate(double time, double quarters, double tempo, double rate) noexcept
Definition MIDIClockEngine.hpp:351
void process(double current_time, double frame_duration, int64_t frames, double tempo, double factor, double offset, Emit &&emit)
factor: nudge times clock rate; offset in seconds.
Definition MIDIClockEngine.hpp:364