120 halp_meta(name,
"MIDI Sync Out")
121 halp_meta(author,
"ossia team")
122 halp_meta(c_name,
"avnd_helpers_midisync")
123 halp_meta(manual_url,
"https://ossia.io/score-docs/processes/midi-sync.html")
124 halp_meta(uuid,
"aa7c1ae5-495e-436e-a079-e3f1a19861bb")
125 halp_meta(category,
"Timing/Midi")
126 halp_meta(description,
"Send MIDI Clock, transport messages and MIDI Time Code synchronized to score playback.")
127 halp_flag(process_exec);
129 ossia::exec_state_facade ossia_state;
141 halp::enum_t<MidiClockMode,
"MIDI Clock"> clock;
142 halp::enum_t<MidiStartStopMode,
"MIDI Start/Stop"> clock_startstop;
143 halp::enum_t<MidiTimeCodeMode,
"MIDI TimeCode"> mtc;
145 halp::spinbox_i32<
"MTC offset (s)", halp::irange{-128000, 128000, 0}> offset;
146 struct : halp::enum_t<MidiTimeCodeFrameRate,
"MTC rate">
150 std::string_view values[4] = {
"24",
"25",
"29.97",
"30"};
151 MidiTimeCodeFrameRate init{};
155 struct : halp::enum_t<MidiClockSource,
"Clock source">
159 "Score: clock and transport follow score playback. Free: the clock runs at "
160 "its own tempo, driven by Start, Stop, Continue and Tap")
163 std::string_view values[2] = {
"Score",
"Free"};
164 MidiClockSource init{};
168 struct : halp::spinbox_f32<
"BPM", halp::range{20., 300., 120.}>
170 halp_meta(description,
"Tempo of the free clock, until a tempo is tapped")
173 struct : halp::impulse_button<
"Start">
175 halp_meta(description,
"Free clock: send Start and clock from the beginning")
178 struct : halp::impulse_button<
"Stop">
180 halp_meta(description,
"Free clock: send Stop and stop the clock")
183 struct : halp::impulse_button<
"Continue">
185 halp_meta(description,
"Free clock: send Continue and clock from where it stopped")
188 struct : halp::impulse_button<
"Tap">
192 "Free clock: tap the tempo (average of the last 32 taps, a pause of 3 s "
193 "starts over). When stopped, 4 taps start the clock on the next beat")
196 struct : halp::maintained_button<
"Nudge down">
198 halp_meta(description,
"Clock 10% slower while held, to move its phase back")
201 struct : halp::maintained_button<
"Nudge up">
203 halp_meta(description,
"Clock 10% faster while held, to move its phase forward")
206 struct : halp::enum_t<MidiClockRate,
"Clock rate">
208 halp_meta(description,
"Multiplies the number of clocks sent per beat")
211 std::string_view values[7] = {
"x1",
"x1/4",
"x1/3",
"x1/2",
"x2",
"x3",
"x4"};
212 MidiClockRate init{};
216 struct : halp::spinbox_f32<
"Clock offset (ms)", halp::range{-1000., 1000., 0.}>
220 "Positive values delay the clock and the transport messages. Negative values "
221 "send the score clock early by extrapolating the current tempo, on the output "
222 "port or to a MIDI device")
228 struct : halp::midi_bus<
"MIDI output">
230 ossia::net::node_base* ossia_node{};
233 struct : halp::val_port<
"Tempo",
float>
235 halp_meta(description,
"Tempo of the clock before nudge and rate")
241 halp_meta(name,
"MIDI Sync Out")
242 halp_meta(layout, halp::layouts::hbox)
243 halp_meta(background, halp::colors::background_dark)
247 halp_meta(name,
"Send")
248 halp_meta(layout, halp::layouts::section)
249 halp_meta(background, halp::colors::background_mid)
250 halp::item<&ins::clock> clock;
251 halp::item<&ins::clock_startstop> clock_startstop;
252 halp::item<&ins::mtc> mtc;
253 halp::item<&ins::offset> offset;
254 halp::item<&ins::rate> rate;
259 halp_meta(name,
"Clock")
260 halp_meta(layout, halp::layouts::section)
261 halp_meta(background, halp::colors::background_mid)
262 halp::item<&ins::source> source;
263 halp::item<&ins::clock_rate, halp::look{.widget = halp::control_widget::combo}>
265 halp::item<&ins::clock_offset> clock_offset;
270 halp_meta(name,
"Free clock")
271 halp_meta(layout, halp::layouts::section)
272 halp_meta(background, halp::colors::background_mid)
273 static constexpr auto condition()
275 return halp::enabled_when<&ins::source, MidiClockSource::Free>{};
277 halp::item<&ins::bpm> bpm;
280 halp_meta(layout, halp::layouts::hbox)
281 halp::item<&ins::start_button> start;
282 halp::item<&ins::stop_button> stop;
283 halp::item<&ins::continue_button> cont;
287 halp_meta(layout, halp::layouts::hbox)
288 halp::item<&ins::tap> tap;
289 halp::item<&ins::nudge_down> nudge_down;
290 halp::item<&ins::nudge_up> nudge_up;
296 std::atomic_bool free_mode{
false};
297 std::atomic_bool free_send_clock{
false};
298 std::atomic_bool free_send_transport{
false};
299 std::atomic<float> free_bpm{120.f};
300 std::atomic<float> clock_rate_factor{1.f};
301 std::atomic<int> nudge{0};
302 std::atomic<float> clock_offset_ms{0.f};
303 std::atomic<midi_clock::transport_command> free_command{
304 midi_clock::transport_command::None};
319 ossia::net::midi::midi_stream* device{};
335 uint8_t frame_rate = 0b10;
342 using tick = halp::tick_flicks;
343 std::thread clock_thread;
344 std::atomic_bool clock_thread_running =
true;
348 std::atomic_bool stop_requested{
false};
349 std::atomic_bool pause_requested{
false};
351 static void send_midi(
352 ossia::net::midi::midi_stream* device, std::span<const uint8_t> bytes)
355 device->push_value(libremidi::message{{std::begin(bytes), std::end(bytes)}, 0});
359 static constexpr auto from_mtc_framerate(uint32_t frame_rate)
375 static int64_t steady_now_ns() noexcept
377 return std::chrono::duration_cast<std::chrono::nanoseconds>(
378 std::chrono::steady_clock::now().time_since_epoch())
382 midi_clock::free_clock_params load_free_params() const noexcept
384 return midi_clock::free_clock_params{
385 .bpm = free_bpm.load(std::memory_order_relaxed),
386 .rate = clock_rate_factor.load(std::memory_order_relaxed),
387 .nudge = nudge.load(std::memory_order_relaxed),
388 .offset_ms = clock_offset_ms.load(std::memory_order_relaxed),
389 .send_clock = free_send_clock.load(std::memory_order_relaxed),
390 .send_transport = free_send_transport.load(std::memory_order_relaxed)};
393 void send_free_clock_message(
394 ossia::net::midi::midi_stream* device,
const midi_clock::message& m)
397 send_midi(device, std::span<const uint8_t>(m.bytes.data(), m.size));
409 using namespace std::chrono;
410 ossia::set_thread_name(
"ossia midi clock");
411 ossia::set_thread_pinned(ossia::thread_type::Midi, 0);
415 bool was_free =
false;
417 bool has_pending =
false;
418 ossia::net::midi::midi_stream* device{};
420 send_free_clock_message(device, m);
423 while(clock_thread_running.load(std::memory_order_acquire))
425 const auto p = load_free_params();
426 const int64_t now = steady_now_ns();
427 int64_t deadline = midi_clock::free_clock::never;
428 if(free_mode.load(std::memory_order_acquire))
431 if(
auto c = free_command.exchange(midi_clock::transport_command::None);
432 c != midi_clock::transport_command::None)
433 free_clock.command(c, now, p, send);
435 free_clock.tap(t, p);
437 free_clock.
process(now, p, send);
446 free_clock.command(midi_clock::transport_command::Stop, now, p, send);
448 free_command.exchange(midi_clock::transport_command::None);
455 has_pending =
to_device.try_dequeue(pending);
460 device = pending.device;
464 if(pending.msg.at > steady_now_ns())
466 deadline = std::min(deadline, pending.msg.at);
469 send_midi(device, std::span<const uint8_t>(pending.msg.bytes.data(), pending.msg.size));
474 const int64_t wait = deadline - steady_now_ns();
476 std::this_thread::sleep_for(milliseconds(1));
478 precise_sleep(wait / 1e9);
489 clock_thread_running.store(
false, std::memory_order_release);
493 void start() {
start_requested.store(
true, std::memory_order_release); }
494 void stop() { stop_requested.store(
true, std::memory_order_release); }
495 void pause() { pause_requested.store(
true, std::memory_order_release); }
500 mtc_time make_mtc_time(
double total_seconds)
const noexcept
504 =
static_cast<std::underlying_type_t<MidiTimeCodeFrameRate>
>(inputs.rate.value)
507 total_seconds += this->inputs.offset;
508 if(total_seconds < 0.)
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);
521 t.f = std::min(uint8_t(from_mtc_framerate(t.frame_rate) * frames), uint8_t(29));
526 void prepare(halp::setup s) { setup = s; }
528 void operator()(halp::tick_flicks tk)
536 const bool is_free = inputs.source.value == MidiClockSource::Free;
542 const bool stopping = stop_requested.exchange(
false, std::memory_order_acq_rel);
543 const bool pausing = pause_requested.exchange(
false, std::memory_order_acq_rel);
544 if(stopping || pausing)
549 if(main_state.
running && !is_free
550 && inputs.clock_startstop == MidiStartStopMode::Enabled)
552 if(main_state.to_device)
565 process_timeline(tk, is_free);
570 outputs.tempo.value = free_mode.load(std::memory_order_relaxed)
577 const int64_t stale_before
578 = steady_now_ns() - 2 * main_state.latency_ns - 2'000'000;
579 midi_clock::message m;
582 if(m.at < stale_before)
585 msg.bytes.assign(m.bytes.begin(), m.bytes.begin() + m.size);
587 outputs.midi.push_back(std::move(msg));
593 if(stopping || pausing)
599 void set_device(ossia::net::midi::midi_stream* stream)
noexcept
613 ossia::net::midi::midi_stream* stream{};
614 if(outputs.midi.ossia_node)
616 auto& proto = outputs.midi.ossia_node->get_device().get_protocol();
617 stream =
dynamic_cast<ossia::net::midi::midi_stream*
>(&proto);
625 const bool is_free = inputs.source.value == MidiClockSource::Free;
626 free_send_clock.store(inputs.clock == MidiClockMode::Enabled, std::memory_order_relaxed);
627 free_send_transport.store(
628 inputs.clock_startstop == MidiStartStopMode::Enabled, std::memory_order_relaxed);
629 free_bpm.store(inputs.bpm.value, std::memory_order_relaxed);
630 clock_rate_factor.store(
632 std::memory_order_relaxed);
634 int(inputs.nudge_up.value) -
int(inputs.nudge_down.value),
635 std::memory_order_relaxed);
636 clock_offset_ms.store(inputs.clock_offset.value, std::memory_order_relaxed);
637 free_mode.store(is_free, std::memory_order_release);
642 using enum midi_clock::transport_command;
643 if(inputs.stop_button)
644 free_command.store(Stop, std::memory_order_release);
645 else if(inputs.start_button)
646 free_command.store(Start, std::memory_order_release);
647 else if(inputs.continue_button)
648 free_command.store(Continue, std::memory_order_release);
652 const int64_t t = steady_now_ns();
653 pending_tap.store(t != 0 ? t : 1, std::memory_order_release);
666 const int64_t now = steady_now_ns();
667 const double ns_per_frame = 1e9 / setup.rate;
668 const int64_t buffer_ns = int64_t(std::max(setup.frames, frames) * ns_per_frame);
669 const int64_t drift = now - main_state.next_tick_ns;
670 if(main_state.next_tick_ns == 0 || std::abs(drift) > buffer_ns + 2'000'000)
671 main_state.tick_start_ns = now;
673 main_state.tick_start_ns = main_state.next_tick_ns + drift / 16;
674 main_state.ns_per_frame = ns_per_frame;
675 main_state.latency_ns = buffer_ns;
676 main_state.next_tick_ns
677 = main_state.tick_start_ns + int64_t(std::max(frames, 0) * ns_per_frame);
678 main_state.to_device =
midi_out !=
nullptr;
679 main_state.tick_frames = std::max(frames, 0);
683 void emit(int64_t frame, std::initializer_list<uint8_t> bytes)
noexcept
685 if(main_state.to_device)
689 m.
at = main_state.tick_start_ns + main_state.latency_ns
690 + int64_t(frame * main_state.ns_per_frame);
691 m.size = int(std::min(bytes.size(), m.bytes.size()));
692 std::copy_n(bytes.begin(), m.size, m.bytes.begin());
698 msg.bytes.assign(bytes.begin(), bytes.end());
699 msg.timestamp = frame;
700 outputs.midi.push_back(std::move(msg));
712 const double offset = inputs.clock_offset.value / 1000.;
713 const int64_t delay = offset > 0. ? int64_t(std::llround(offset * setup.rate)) : 0;
714 if(main_state.to_device || delay < main_state.tick_frames)
720 auto& d = main_state.delayed;
721 if(main_state.delayed_count ==
int(d.size()))
724 emit(std::max<int64_t>(main_state.tick_frames - 1, 0), bytes);
727 auto& e = d[main_state.delayed_count++];
729 e.size = int(std::min(bytes.size(), e.bytes.size()));
730 std::copy_n(bytes.begin(), e.size, e.bytes.begin());
736 msg.bytes.assign(m.bytes.begin(), m.bytes.begin() + m.size);
737 msg.timestamp = frame;
738 outputs.midi.push_back(std::move(msg));
744 const int64_t end = main_state.
out_frame + std::max(frames, 0);
746 for(
int i = 0; i < main_state.delayed_count; i++)
748 auto& e = main_state.delayed[i];
750 emit_message(std::max<int64_t>(e.at - main_state.
out_frame, 0), e);
752 main_state.delayed[kept++] = e;
754 main_state.delayed_count = kept;
757 void flush_delayed() noexcept
759 for(
int i = 0; i < main_state.delayed_count; i++)
760 emit_message(0, main_state.delayed[i]);
761 main_state.delayed_count = 0;
767 auto& v = outputs.midi.midi_messages;
768 for(std::size_t i = 1; i < v.size(); i++)
769 for(std::size_t j = i; j > 0 && v[j - 1].timestamp > v[j].timestamp; j--)
770 std::swap(v[j - 1], v[j]);
773 void process_timeline(
const halp::tick_flicks& tk,
bool is_free)
775 const double current_time = tk.start_in_flicks / 705'600'000.0;
776 const double frame_duration
777 = std::abs(tk.end_in_flicks - tk.start_in_flicks) / 705'600'000.0;
778 const auto time = make_mtc_time(current_time);
781 int(inputs.nudge_up.value) -
int(inputs.nudge_down.value))
787 = main_state.
running && tk.start_in_flicks != main_state.next_start_in_flicks;
788 if(!main_state.
running || jumped)
789 locate(tk, time, current_time, factor, jumped, is_free);
791 main_state.next_start_in_flicks = tk.end_in_flicks;
793 if(!is_free && inputs.clock == MidiClockMode::Enabled)
796 current_time, frame_duration, tk.frames, tk.tempo, factor,
797 inputs.clock_offset.value / 1000.,
798 [
this](int64_t sample_offset) { emit(sample_offset, {0xF8}); });
801 if(inputs.mtc == MidiTimeCodeMode::Enabled)
812 const halp::tick_flicks& tk,
const mtc_time& time,
double current_time,
813 double factor,
bool jumped,
bool is_free)
816 = !is_free && inputs.clock_startstop == MidiStartStopMode::Enabled;
817 const bool clock = !is_free && inputs.clock == MidiClockMode::Enabled;
818 const double quarters = tk.start_position_in_quarters;
819 const bool at_start = std::abs(quarters) < 1e-9;
821 if(jumped && transport)
822 emit_transport({0xFC});
826 const int64_t beats = main_state.clock.locate(
827 current_time, quarters, tk.tempo,
830 if(clock || transport)
834 if(!(transport && at_start) && beats >= 0 && beats < 16384)
835 emit_transport({0xF2, uint8_t(beats & 0x7F), uint8_t((beats >> 7) & 0x7F)});
837 emit_transport({uint8_t(at_start ? 0xFA : 0xFB)});
840 if(inputs.mtc == MidiTimeCodeMode::Enabled)
843 0, {0xF0, 0x7F, 0x7F, 0x01, 0x01, uint8_t(time.h | (time.frame_rate << 5)),
844 time.m, time.s, time.f, 0xF7});
846 const double fps = from_mtc_framerate(time.frame_rate);
847 main_state.mtc_quarter_frame_index = 0;
848 main_state.last_mtc_quarter_frame_time
849 = next_even_frame(current_time, fps) - 1.0 / (fps * 4.0);
859 const double offset = this->inputs.offset;
860 const double u = t + offset;
861 const double second = std::floor(u);
863 const int per_second = int(std::ceil(fps - 1e-9));
864 int k = int(std::ceil((u - second) * fps - 1e-6));
867 const double frame = k < per_second ? second + k / fps : second + 1.;
868 return frame - offset;
877 const halp::tick_flicks& tk,
const mtc_time& time,
double current_time,
878 double frame_duration)
880 const double fps = from_mtc_framerate(time.frame_rate);
881 const double seconds_per_quarter_frame = 1.0 / (fps * 4.0);
884 = main_state.last_mtc_quarter_frame_time + seconds_per_quarter_frame;
886 while(next_qf_time < current_time + frame_duration)
889 if(next_qf_time >= current_time - 1e-9)
891 int64_t sample_offset
892 = ((next_qf_time - current_time) / frame_duration) * tk.frames;
893 sample_offset = std::clamp<int64_t>(sample_offset, 0, tk.frames - 1);
896 if(main_state.mtc_quarter_frame_index == 0)
898 = make_mtc_time(next_qf_time + seconds_per_quarter_frame / 2.);
899 const mtc_time& cycle = main_state.mtc_cycle;
902 switch(main_state.mtc_quarter_frame_index)
905 qf_data = 0x00 | (cycle.f & 0x0F);
908 qf_data = 0x10 | ((cycle.f >> 4) & 0x01);
911 qf_data = 0x20 | (cycle.s & 0x0F);
914 qf_data = 0x30 | ((cycle.s >> 4) & 0x03);
917 qf_data = 0x40 | (cycle.m & 0x0F);
920 qf_data = 0x50 | ((cycle.m >> 4) & 0x03);
923 qf_data = 0x60 | (cycle.h & 0x0F);
926 qf_data = 0x70 | (cycle.frame_rate << 1) | ((cycle.h >> 4) & 0x01);
930 emit(sample_offset, {0xF1, qf_data});
932 main_state.mtc_quarter_frame_index
933 = (main_state.mtc_quarter_frame_index + 1) % 8;
936 main_state.last_mtc_quarter_frame_time = next_qf_time;
937 next_qf_time += seconds_per_quarter_frame;
945 double last_mtc_quarter_frame_time = 0.0;
946 int mtc_quarter_frame_index = 0;
950 bool running =
false;
951 int64_t next_start_in_flicks = 0;
954 int64_t out_frame = 0;
956 std::array<midi_clock::message, 32> delayed{};
957 int delayed_count = 0;
959 bool to_device =
false;
960 int64_t tick_start_ns = 0;
961 int64_t next_tick_ns = 0;
962 int64_t latency_ns = 0;
963 double ns_per_frame = 0.;
int64_t out_frame
Frames of output since the object started, and in the current tick.
Definition MIDISync.hpp:954