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