Loading...
Searching...
No Matches
MIDISync.hpp
1#pragma once
2
3/* SPDX-License-Identifier: GPL-3.0-or-later */
4
5#include <ossia/dataflow/exec_state_facade.hpp>
6#include <ossia/detail/thread.hpp>
7#include <ossia/network/base/device.hpp>
8#include <ossia/network/base/protocol.hpp>
9#include <ossia/protocols/midi/midi_protocol.hpp>
10
11#include <halp/audio.hpp>
12#include <halp/controls.hpp>
13#include <halp/file_port.hpp>
14#include <halp/meta.hpp>
15#include <halp/midi.hpp>
16#include <halp/midifile_port.hpp>
17#include <libremidi/message.hpp>
18
19#include <cmath>
20
21#include <thread>
22
23namespace mtk
24{
25enum class MidiClockMode
26{
27 Disabled,
28 Enabled
29};
30enum class MidiStartStopMode
31{
32 Disabled,
33 Enabled
34};
35enum class MidiTimeCodeMode
36{
37 Disabled,
38 Enabled
39};
40enum class MidiTimeCodeFrameRate
41{
42 SMPTE_24 = 0b00,
43 SMPTE_25 = 0b01,
44 SMPTE_30 = 0b10,
45 SMPTE_2997 = 0b11
46};
47
48enum class MidiStartStopEvent
49{
50 None,
51 Start,
52 Stop,
53 Continue
54};
55
56// https://blat-blatnik.github.io/computerBear/making-accurate-sleep-function/
58{
59 double estimate = 5e-3;
60 double mean = 5e-3;
61 double m2 = 0;
62 int64_t count = 1;
63
64 void operator()(double seconds)
65 {
66 using namespace std;
67 using namespace std::chrono;
68
69 while(seconds > estimate)
70 {
71 auto start = high_resolution_clock::now();
72 this_thread::sleep_for(milliseconds(1));
73 auto end = high_resolution_clock::now();
74
75 double observed = (end - start).count() / 1e9;
76 seconds -= observed;
77
78 ++count;
79 double delta = observed - mean;
80 mean += delta / count;
81 m2 += delta * (observed - mean);
82 double stddev = std::sqrt(m2 / (count - 1));
83 estimate = mean + stddev;
84
85 // FIXME that's missing a rolling behaviour to be more
86 // precise for semi-large timescales, e.g.
87 // unplugging a laptop and powersave changing frequency
88 }
89
90 // spin lock
91 auto start = high_resolution_clock::now();
92 while((high_resolution_clock::now() - start).count() / 1e9 < seconds)
93 ;
94 }
95};
96
101{
102 halp_meta(name, "Midi sync")
103 halp_meta(author, "ossia team")
104 halp_meta(c_name, "avnd_helpers_midisync")
105 halp_meta(manual_url, "https://ossia.io/score-docs/processes/midi-sync.html")
106 halp_meta(uuid, "aa7c1ae5-495e-436e-a079-e3f1a19861bb")
107 halp_meta(category, "Midi")
108 halp_flag(process_exec);
109
110 ossia::exec_state_facade ossia_state;
111 std::atomic<ossia::net::midi::midi_protocol*> midi_out{};
112 std::atomic<MidiStartStopEvent> next_event_midiclock{};
113 std::atomic<MidiStartStopEvent> next_event_mtc{};
114 std::atomic<double> current_song_pos{};
115
116 struct
117 {
118 halp::enum_t<MidiClockMode, "MIDI Clock"> clock;
119 halp::enum_t<MidiStartStopMode, "MIDI Start/Stop"> clock_startstop;
120 halp::enum_t<MidiTimeCodeMode, "MIDI TimeCode"> mtc;
121 //halp::spinbox_i32<"Channel", halp::irange{1, 16, 1}> channel;
122 halp::spinbox_i32<"MTC offset (s)", halp::irange{-128000, 128000, 0}> offset;
123 struct : halp::enum_t<MidiTimeCodeFrameRate, "MTC rate">
124 {
125 struct range
126 {
127 std::string_view values[4] = {"24", "25", "29.97", "30"};
128 MidiTimeCodeFrameRate init{};
129 };
130 } rate;
131 } inputs;
132
133 struct
134 {
135 struct : halp::midi_bus<"MIDI output">
136 {
137 ossia::net::node_base* ossia_node{};
138 } midi;
139 } outputs;
140
142 {
143 uint64_t u;
144 struct alignas(uint64_t) impl
145 {
146 float tempo = 0.f;
147 uint32_t has_clock : 1 = 0;
148 uint32_t has_startstop : 1 = 0;
149 uint32_t has_mtc : 1 = 0;
150
151 uint32_t frame_rate : 2 = 0b10;
152 uint32_t h : 5 = 0;
153 uint32_t m : 6 = 0;
154 uint32_t s : 6 = 0;
155 uint32_t f : 5 = 0;
156 };
157 static_assert(sizeof(impl) == 8);
158 };
159
160 using tick = halp::tick_flicks;
161 std::thread clock_thread;
162 std::thread mtc_thread;
163 std::atomic_bool clock_thread_running = true;
164 std::atomic_bool mtc_thread_running = true;
165
166 std::atomic<uint64_t> current_state = 0;
167
168 template <typename... T>
169 void send_midi(T... bytes)
170 requires(!(std::is_pointer_v<T> || ...))
171 {
172 if(auto proto = midi_out.load())
173 proto->push_value(libremidi::message{
174 libremidi::midi_bytes{static_cast<unsigned char>(bytes)...}, 0});
175 }
176
177 void send_midi(std::span<const uint8_t> bytes)
178 {
179 if(auto proto = midi_out.load())
180 proto->push_value(libremidi::message{{std::begin(bytes), std::end(bytes)}, 0});
181 }
182
183 [[nodiscard]]
184 auto load_state() noexcept
185 {
186 auto u = this->current_state.load(std::memory_order_acquire);
187 auto state = std::bit_cast<storage::impl>(u);
188 if(state.tempo <= 0.)
189 state.tempo = 120.;
190 return state;
191 }
192
193 [[nodiscard]]
194 static auto compute_time_between_ticks(storage::impl state) noexcept
195 {
196 const double duration_of_quarter_note_in_seconds = 60. / state.tempo;
197 const std::chrono::nanoseconds time_between_ticks = std::chrono::nanoseconds(
198 int64_t(1e9 * duration_of_quarter_note_in_seconds / 24.));
199 return time_between_ticks;
200 };
201
202 void full_songpos_message(double quarters)
203 {
204 // A midi beat = a 16th note
205 // Note that this means due to having only 14 bits of storage,
206 // that a song is limited to 1024 bars.. half an hour at 120 bpm lol
207 // To prevent unwanted looping we will make the editorial choice to not send the message
208 // if it ends up > to that limit
209 double midi_beats = quarters * 4.;
210
211 uint64_t res = std::floor(midi_beats);
212 if(res < 16384)
213 {
214 // 0b0111'1111 0b0001'1111
215 uint8_t message[3] = {0xF2, 0x00, 0x00};
216 message[1] = (res & 0b0011'1111'1000'0000) >> 7;
217 message[2] = (res & 0b0000'0000'0111'1111);
218 send_midi(message);
219 }
220 }
221
222 void full_mtc_message(storage::impl state)
223 {
224 static_assert(0b0000'0011 << 5 == 0b01100000);
225 const uint8_t h = state.h | (state.frame_rate << 5);
226 const uint8_t m = state.m;
227 const uint8_t s = state.s;
228 const uint8_t f = state.f;
229
230 const uint8_t bytes[10]{0xF0, 0x7F, 0x7F, 0x01, 0x00, h, m, s, f, 0xF7};
231 send_midi(bytes);
232 }
233
234 void current_mtc_message(int& index, storage::impl state)
235 {
236 uint8_t bytes[2]{0xF1, 0};
237 // thanks wikipedia my good friend i promise i will donate
238 // 0 0000 ffff Frame number lsbits
239 // 1 0001 000f Frame number msbit
240 // 2 0010 ssss Second lsbits
241 // 3 0011 00ss Second msbits
242 // 4 0100 mmmm Minute lsbits
243 // 5 0101 00mm Minute msbits
244 // 6 0110 hhhh Hour lsbits
245 // 7 0111 0rrh Rate and hour msbit
246 switch(index)
247 {
248 case 0:
249 bytes[1] = 0b0000'0000 | (0b1111 & state.f);
250 index++;
251 break;
252 case 1:
253 bytes[1] = 0b0001'0000 | (0b0001 & (state.f >> 4));
254 index++;
255 break;
256 case 2:
257 bytes[1] = 0b0010'0000 | (0b1111 & state.s);
258 index++;
259 break;
260 case 3:
261 bytes[1] = 0b0011'0000 | (0b0011 & (state.s >> 4));
262 index++;
263 break;
264 case 4:
265 bytes[1] = 0b0100'0000 | (0b1111 & state.m);
266 index++;
267 break;
268 case 5:
269 bytes[1] = 0b0101'0000 | (0b0011 & (state.m >> 4));
270 index++;
271 break;
272 case 6:
273 bytes[1] = 0b0110'0000 | (0b0011 & state.h);
274 index++;
275 break;
276 case 7:
277 bytes[1] = 0b0111'0000 | (state.frame_rate << 1) | (0b0001 & (state.h >> 4));
278 index = 0;
279 break;
280 }
281 send_midi(bytes);
282 }
283
284 [[nodiscard]]
285 static constexpr auto from_mtc_framerate(uint32_t frame_rate)
286 {
287 switch(frame_rate)
288 {
289 case 0b00:
290 return 24.;
291 break;
292 case 0b01:
293 return 25.;
294 break;
295 case 0b10:
296 return 30.;
297 break;
298 case 0b11:
299 return 29.97;
300 break;
301 }
302 return 30.;
303 }
304
305 MidiSync()
306 {
307 // Midi Clock handling
308 clock_thread = std::thread{[this] {
309 ossia::set_thread_name("ossia midi clock");
310 ossia::set_thread_pinned(ossia::thread_type::Midi, 0);
311
312 sleep_accurate precise_sleep;
313
314 std::chrono::steady_clock::time_point last_tick_sent{}, now{};
315 // Send one now
316 last_tick_sent = std::chrono::steady_clock::now();
317 now = last_tick_sent;
318
319 // if(load_state().has_clock)
320 // send_midi(0xF8);
321
322 while(clock_thread_running.load(std::memory_order_acquire))
323 {
324 auto state = load_state();
325 auto msg_to_send = this->next_event_midiclock.exchange(MidiStartStopEvent::None);
326 if(state.has_startstop)
327 {
328 switch(msg_to_send)
329 {
330 case MidiStartStopEvent::None:
331 break;
332 case MidiStartStopEvent::Start:
333 full_songpos_message(0.);
334 send_midi(0xFA);
335 if(state.has_clock)
336 {
337 send_midi(0xF8);
338 last_tick_sent = std::chrono::steady_clock::now();
339 now = last_tick_sent;
340 }
341 break;
342 case MidiStartStopEvent::Continue:
343 full_songpos_message(
344 this->current_song_pos.load(std::memory_order_acquire));
345 send_midi(0xFB);
346 break;
347 case MidiStartStopEvent::Stop:
348 full_songpos_message(0.);
349 send_midi(0xFC);
350 break;
351 }
352 }
353
354 if(state.has_clock)
355 {
356 auto time_between_ticks = compute_time_between_ticks(state);
357 auto elapsed_nsecs = std::chrono::duration_cast<std::chrono::nanoseconds>(
358 now - last_tick_sent);
359
360 if(elapsed_nsecs < time_between_ticks)
361 precise_sleep((time_between_ticks - elapsed_nsecs).count() / 1e9);
362
363 send_midi(0xF8);
364
365 last_tick_sent = now;
366 }
367 else
368 {
369 std::this_thread::sleep_for(std::chrono::milliseconds(3));
370 }
371 }
372 }};
373
374 // Midi Clock handling
375 mtc_thread = std::thread{[this] {
376 ossia::set_thread_name("ossia midi mtc");
377 ossia::set_thread_pinned(ossia::thread_type::Midi, 0);
378
379 sleep_accurate precise_sleep;
380
381 std::chrono::steady_clock::time_point last_tick_sent{}, now{};
382 // Send one now
383 last_tick_sent = std::chrono::steady_clock::now();
384 now = last_tick_sent;
385
386 storage::impl state;
387 int current_index = 0;
388 if(state = load_state(); state.has_mtc)
389 current_mtc_message(current_index, state);
390
391 while(mtc_thread_running.load(std::memory_order_acquire))
392 {
393 auto new_state = load_state();
394 auto msg_to_send = this->next_event_mtc.exchange(MidiStartStopEvent::None);
395 if(state.has_startstop)
396 {
397 switch(msg_to_send)
398 {
399 case MidiStartStopEvent::None:
400 break;
401 case MidiStartStopEvent::Start:
402 full_mtc_message(make_state(state.tempo, 0.));
403 break;
404 case MidiStartStopEvent::Continue:
405 full_mtc_message(state);
406 break;
407 case MidiStartStopEvent::Stop:
408 full_mtc_message(make_state(state.tempo, 0.));
409 break;
410 }
411 }
412
413 if(new_state.has_mtc)
414 {
415 // We don't want to change the timing in the middle
416 // of packets
417 if(current_index == 0)
418 state = new_state;
419
420 double frame_rate = from_mtc_framerate(state.frame_rate);
421 // We send messages in quarter frames
422 frame_rate *= 4.;
423
424 auto time_between_ticks = std::chrono::nanoseconds(int64_t(1e9 / frame_rate));
425 auto elapsed_nsecs = std::chrono::duration_cast<std::chrono::nanoseconds>(
426 now - last_tick_sent);
427
428 if(elapsed_nsecs < time_between_ticks)
429 precise_sleep((time_between_ticks - elapsed_nsecs).count() / 1e9);
430
431 current_mtc_message(current_index, state);
432
433 last_tick_sent = now;
434 }
435 else
436 {
437 current_index = 0;
438 state = new_state;
439 std::this_thread::sleep_for(std::chrono::milliseconds(3));
440 }
441 }
442 }};
443 }
444
445 ~MidiSync()
446 {
447 clock_thread_running.store(false, std::memory_order_release);
448 mtc_thread_running.store(false, std::memory_order_release);
449 clock_thread.join();
450 mtc_thread.join();
451 }
452
453 void start()
454 {
455 next_event_midiclock.store(MidiStartStopEvent::Start, std::memory_order_release);
456 current_song_pos.store(0., std::memory_order_release);
457 }
458
459 void stop()
460 {
461 if(inputs.clock_startstop == MidiStartStopMode::Enabled)
462 send_midi(0xFC);
463
464 next_event_midiclock.store(MidiStartStopEvent::Stop, std::memory_order_release);
465 current_song_pos.store(0., std::memory_order_release);
466 }
467
468 void pause()
469 {
470 auto u = this->current_state.load(std::memory_order_acquire);
471 auto state = std::bit_cast<storage::impl>(u);
472 state.has_clock = false;
473 state.has_mtc = false;
474 this->current_state.store(std::bit_cast<uint64_t>(state), std::memory_order_release);
475 }
476
477 void resume()
478 {
479 next_event_midiclock.store(MidiStartStopEvent::Continue, std::memory_order_release);
480
481 auto u = this->current_state.load(std::memory_order_acquire);
482 auto state = std::bit_cast<storage::impl>(u);
483 state.has_clock = inputs.clock == MidiClockMode::Enabled;
484 state.has_mtc = inputs.mtc == MidiTimeCodeMode::Enabled;
485 this->current_state.store(std::bit_cast<uint64_t>(state), std::memory_order_release);
486 }
487
488 void transport(auto time)
489 {
490 // FIXME
491 /*
492 auto nsec = std::chrono::duration_cast<std::chrono::nanoseconds>(time);
493 auto state = make_state(0., nsec.count() / 1e9);
494 if(inputs.mtc == MidiTimeCodeMode::Emit)
495 full_mtc_message(state);
496
497 if(inputs.clock == MidiClockMode::Emit)
498 full_songpos_message(state);
499 */
500 }
501
502 [[nodiscard]]
503 storage::impl make_state(double tempo, double total_seconds)
504 {
505 storage::impl state;
506 state.tempo = tempo;
507
508 total_seconds += this->inputs.offset;
509
510 auto h = std::div((long long)total_seconds, (long long)3600).quot;
511 total_seconds -= h * 3600;
512 auto m = std::div((long long)total_seconds, (long long)60).quot;
513 total_seconds -= m * 60;
514 float s;
515 auto frames = std::modf(total_seconds, &s);
516
517 state.has_clock = inputs.clock == MidiClockMode::Enabled;
518 state.has_startstop = inputs.clock_startstop == MidiStartStopMode::Enabled;
519 state.has_mtc = inputs.mtc == MidiTimeCodeMode::Enabled;
520 state.frame_rate
521 = static_cast<std::underlying_type_t<MidiTimeCodeFrameRate>>(inputs.rate.value);
522 state.h = h % 24;
523 state.m = m % 60;
524 state.s = std::floor(s);
525 state.f = from_mtc_framerate(state.frame_rate) * frames;
526
527 return state;
528 }
529
530 halp::setup setup;
531 void prepare(halp::setup s) { setup = s; }
532 void operator()(halp::tick_flicks tk)
533 {
534 if(setup.rate <= 0)
535 return;
536
537 if(outputs.midi.ossia_node)
538 {
539 auto& proto = outputs.midi.ossia_node->get_device().get_protocol();
540 if(auto mp = dynamic_cast<ossia::net::midi::midi_protocol*>(&proto))
541 midi_out = mp;
542 }
543
544 auto state = make_state(tk.tempo, tk.start_in_flicks / 705'600'000.);
545 // FIXME midi out : mpmc for output
546 // Global MTC start / stop input has to be done in a device
547
548 this->current_state.store(std::bit_cast<uint64_t>(state), std::memory_order_release);
549 this->current_song_pos.store(tk.start_position_in_quarters);
550 }
551};
552}
STL namespace.
Definition MIDISync.hpp:145
Definition MIDISync.hpp:101
Definition MIDISync.hpp:58
Definition MIDISync.hpp:126
Definition MIDISync.hpp:142