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);
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{};
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;
123 halp::spinbox_i32<
"MTC offset (s)", halp::irange{-128000, 128000, 0}> offset;
124 struct : halp::enum_t<MidiTimeCodeFrameRate,
"MTC rate">
128 std::string_view values[4] = {
"24",
"25",
"29.97",
"30"};
129 MidiTimeCodeFrameRate init{};
136 struct : halp::midi_bus<
"MIDI output">
138 ossia::net::node_base* ossia_node{};
148 uint32_t has_clock : 1 = 0;
149 uint32_t has_startstop : 1 = 0;
150 uint32_t has_mtc : 1 = 0;
152 uint32_t frame_rate : 2 = 0b10;
158 static_assert(
sizeof(
impl) == 8);
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;
167 std::atomic<uint64_t> current_state = 0;
169 template <
typename... T>
170 void send_midi(T... bytes)
171 requires(!(std::is_pointer_v<T> || ...))
173 if(
auto proto = midi_out.load())
174 proto->push_value(libremidi::message{
175 libremidi::midi_bytes{
static_cast<unsigned char>(bytes)...}, 0});
178 void send_midi(std::span<const uint8_t> bytes)
180 if(
auto proto = midi_out.load())
181 proto->push_value(libremidi::message{{std::begin(bytes), std::end(bytes)}, 0});
185 auto load_state() noexcept
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.)
195 static auto compute_time_between_ticks(storage::impl state)
noexcept
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;
203 void full_songpos_message(
double quarters)
210 double midi_beats = quarters * 4.;
212 uint64_t res = std::floor(midi_beats);
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);
223 void full_mtc_message(storage::impl state)
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;
231 const uint8_t bytes[10]{0xF0, 0x7F, 0x7F, 0x01, 0x00, h, m, s, f, 0xF7};
235 void current_mtc_message(
int& index, storage::impl state)
237 uint8_t bytes[2]{0xF1, 0};
250 bytes[1] = 0b0000'0000 | (0b1111 & state.f);
254 bytes[1] = 0b0001'0000 | (0b0001 & (state.f >> 4));
258 bytes[1] = 0b0010'0000 | (0b1111 & state.s);
262 bytes[1] = 0b0011'0000 | (0b0011 & (state.s >> 4));
266 bytes[1] = 0b0100'0000 | (0b1111 & state.m);
270 bytes[1] = 0b0101'0000 | (0b0011 & (state.m >> 4));
274 bytes[1] = 0b0110'0000 | (0b0011 & state.h);
278 bytes[1] = 0b0111'0000 | (state.frame_rate << 1) | (0b0001 & (state.h >> 4));
286 static constexpr auto from_mtc_framerate(uint32_t frame_rate)
309 clock_thread = std::thread{[
this] {
310 ossia::set_thread_name(
"ossia midi clock");
311 ossia::set_thread_pinned(ossia::thread_type::Midi, 0);
313 sleep_accurate precise_sleep;
315 std::chrono::steady_clock::time_point last_tick_sent{}, now{};
317 last_tick_sent = std::chrono::steady_clock::now();
318 now = last_tick_sent;
323 while(clock_thread_running.load(std::memory_order_acquire))
325 auto state = load_state();
326 auto msg_to_send = this->next_event_midiclock.exchange(MidiStartStopEvent::None);
327 if(state.has_startstop)
331 case MidiStartStopEvent::None:
333 case MidiStartStopEvent::Start:
334 full_songpos_message(0.);
339 last_tick_sent = std::chrono::steady_clock::now();
340 now = last_tick_sent;
343 case MidiStartStopEvent::Continue:
344 full_songpos_message(
345 this->current_song_pos.load(std::memory_order_acquire));
348 case MidiStartStopEvent::Stop:
349 full_songpos_message(0.);
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);
361 if(elapsed_nsecs < time_between_ticks)
362 precise_sleep((time_between_ticks - elapsed_nsecs).count() / 1e9);
366 last_tick_sent = now;
370 std::this_thread::sleep_for(std::chrono::milliseconds(3));
376 mtc_thread = std::thread{[
this] {
377 ossia::set_thread_name(
"ossia midi mtc");
378 ossia::set_thread_pinned(ossia::thread_type::Midi, 0);
380 sleep_accurate precise_sleep;
382 std::chrono::steady_clock::time_point last_tick_sent{}, now{};
384 last_tick_sent = std::chrono::steady_clock::now();
385 now = last_tick_sent;
388 int current_index = 0;
389 if(state = load_state(); state.has_mtc)
390 current_mtc_message(current_index, state);
392 while(mtc_thread_running.load(std::memory_order_acquire))
394 auto new_state = load_state();
395 auto msg_to_send = this->next_event_mtc.exchange(MidiStartStopEvent::None);
396 if(state.has_startstop)
400 case MidiStartStopEvent::None:
402 case MidiStartStopEvent::Start:
403 full_mtc_message(make_state(state.tempo, 0.));
405 case MidiStartStopEvent::Continue:
406 full_mtc_message(state);
408 case MidiStartStopEvent::Stop:
409 full_mtc_message(make_state(state.tempo, 0.));
414 if(new_state.has_mtc)
418 if(current_index == 0)
421 double frame_rate = from_mtc_framerate(state.frame_rate);
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);
429 if(elapsed_nsecs < time_between_ticks)
430 precise_sleep((time_between_ticks - elapsed_nsecs).count() / 1e9);
432 current_mtc_message(current_index, state);
434 last_tick_sent = now;
440 std::this_thread::sleep_for(std::chrono::milliseconds(3));
448 clock_thread_running.store(
false, std::memory_order_release);
449 mtc_thread_running.store(
false, std::memory_order_release);
456 next_event_midiclock.store(MidiStartStopEvent::Start, std::memory_order_release);
457 current_song_pos.store(0., std::memory_order_release);
462 if(inputs.clock_startstop == MidiStartStopMode::Enabled)
465 next_event_midiclock.store(MidiStartStopEvent::Stop, std::memory_order_release);
466 current_song_pos.store(0., std::memory_order_release);
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);
480 next_event_midiclock.store(MidiStartStopEvent::Continue, std::memory_order_release);
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);
489 void transport(
auto time)
504 storage::impl make_state(
double tempo,
double total_seconds)
509 total_seconds += this->inputs.offset;
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;
516 auto frames = std::modf(total_seconds, &s);
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;
522 =
static_cast<std::underlying_type_t<MidiTimeCodeFrameRate>
>(inputs.rate.value);
525 state.s = std::floor(s);
526 state.f = from_mtc_framerate(state.frame_rate) * frames;
532 void prepare(halp::setup s) { setup = s; }
533 void operator()(halp::tick_flicks tk)
538 if(outputs.midi.ossia_node)
540 auto& proto = outputs.midi.ossia_node->get_device().get_protocol();
541 if(
auto mp =
dynamic_cast<ossia::net::midi::midi_stream*
>(&proto))
545 auto state = make_state(tk.tempo, tk.start_in_flicks / 705'600'000.);
549 handle_audio_thread_output(tk, state);
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);
556 handle_audio_thread_output(
const halp::tick_flicks& tk,
const storage::impl& state)
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;
563 if(tk.relative_position == 0 && !main_state.transport_started)
565 if(inputs.clock_startstop == MidiStartStopMode::Enabled)
568 outputs.midi.push_back({.bytes = {0xFA}, .timestamp = 0});
569 main_state.transport_started =
true;
573 if(inputs.clock == MidiClockMode::Enabled)
575 uint16_t pos = tk.start_position_in_quarters * 4;
578 outputs.midi.push_back(
579 {.bytes = {0xF2, uint8_t(pos & 0x7F), uint8_t((pos >> 7) & 0x7F)},
586 if(inputs.clock == MidiClockMode::Enabled)
588 const double seconds_per_tick = 60.0 / (state.tempo * 24.0);
591 double buffer_start_time = current_time;
592 double buffer_end_time = current_time + frame_duration;
595 double next_tick_time = main_state.last_clock_time + seconds_per_tick;
597 while(next_tick_time < buffer_end_time)
599 if(next_tick_time >= buffer_start_time)
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);
606 outputs.midi.push_back(
608 .timestamp = sample_offset});
610 main_state.clock_tick_count++;
613 main_state.last_clock_time = next_tick_time;
614 next_tick_time += seconds_per_tick;
619 if(inputs.mtc == MidiTimeCodeMode::Enabled)
621 double fps = from_mtc_framerate(state.frame_rate);
622 double seconds_per_quarter_frame = 1.0 / (fps * 4.0);
625 = main_state.last_mtc_quarter_frame_time + seconds_per_quarter_frame;
627 while(next_qf_time < current_time + frame_duration)
629 if(next_qf_time >= current_time)
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);
637 switch(main_state.mtc_quarter_frame_index)
640 qf_data = 0x00 | (state.f & 0x0F);
643 qf_data = 0x10 | ((state.f >> 4) & 0x01);
646 qf_data = 0x20 | (state.s & 0x0F);
649 qf_data = 0x30 | ((state.s >> 4) & 0x03);
652 qf_data = 0x40 | (state.m & 0x0F);
655 qf_data = 0x50 | ((state.m >> 4) & 0x03);
658 qf_data = 0x60 | (state.h & 0x0F);
661 qf_data = 0x70 | (state.frame_rate << 1) | ((state.h >> 4) & 0x01);
665 outputs.midi.push_back({.bytes = {0xF1, qf_data}, .timestamp = sample_offset});
667 main_state.mtc_quarter_frame_index
668 = (main_state.mtc_quarter_frame_index + 1) % 8;
671 main_state.last_mtc_quarter_frame_time = next_qf_time;
672 next_qf_time += seconds_per_quarter_frame;
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;