Loading...
Searching...
No Matches
LooperView.hpp
1#pragma once
2#include <Process/Drop/ClipMetadata.hpp>
3
4#include <Fx/Looper/Transport.hpp>
5
6#include <halp/custom_widgets.hpp>
7#include <halp/layout.hpp>
8
9#include <algorithm>
10#include <array>
11#include <chrono>
12#include <cmath>
13#include <cstdint>
14#include <cstdio>
15#include <cstring>
16#include <functional>
17#include <string>
18#include <string_view>
19#include <tuple>
20#include <utility>
21#include <vector>
22
23namespace Nodes::Looper
24{
26struct rgba
27{
28 uint8_t r, g, b, a;
29};
30
32{
34 float start;
35 float end;
36 uint8_t pitch;
37 uint8_t lane;
38 uint8_t velocity;
40 uint8_t heard;
41};
42
45enum class TakeFiles
46{
47 Mix,
48 Layers,
49 Both
50};
51
61{
62 int pass;
63 int layers;
64 bool mute{};
65 int take{};
66 int last{-1};
67};
68
72{
73 static constexpr int max_lanes = 32;
74 static constexpr int bins = 192;
75 static constexpr int max_notes = 768;
76
79 int mode;
80 int requested;
81 int wanted;
82 int layers;
83 int redo;
84 int lanes;
86 int take;
87 int notes;
88 bool empty;
89 bool full;
90 bool writing;
91 bool midi;
95 bool values;
96 float position;
97 float length_seconds;
98 float max_seconds;
99 float tempo;
101 double take_date;
102 std::array<float, max_lanes * bins> peaks;
103 std::array<view_note, max_notes> note;
113 std::array<uint8_t, max_lanes * bins> value_flags;
116 std::array<char, 512> take_base;
117 std::array<char, 8> take_ext;
118 bool take_mix;
119 int take_layers;
125 halp::clip_stretch take_stretch;
128 float speed;
130 std::array<uint8_t, max_lanes> muted;
136 bool new_pass_pending;
143 int take_version;
148 std::array<char, 512> part_base;
149 int part_answered;
150 int part_first;
151 int part_last;
152 int part_answer;
153 bool part_placeable;
154 halp::clip_stretch part_stretch;
160 uint64_t looper;
161 int stops;
162
163 void set_take(
164 std::string_view base, std::string_view ext, bool mix, int layers,
165 halp::clip_stretch stretch = halp::clip_stretch::automatic,
166 bool placeable = true) noexcept
167 {
168 take_placeable = placeable;
169 const auto n = std::min(base.size(), take_base.size() - 1);
170 std::copy_n(base.data(), n, take_base.data());
171 take_base[n] = 0;
172 const auto e = std::min(ext.size(), take_ext.size() - 1);
173 std::copy_n(ext.data(), e, take_ext.data());
174 take_ext[e] = 0;
175 take_mix = mix;
176 take_layers = layers;
177 take_stretch = stretch;
178 }
179
180 void set_part(
181 std::string_view base, int first, int last, int request, halp::clip_stretch stretch,
182 bool placeable) noexcept
183 {
184 const auto n = std::min(base.size(), part_base.size() - 1);
185 std::copy_n(base.data(), n, part_base.data());
186 part_base[n] = 0;
187 part_first = first;
188 part_last = last;
189 part_answer = request;
190 part_stretch = stretch;
191 part_placeable = placeable;
192 }
193
195 std::vector<std::string> layer_files(const char* base_chars, int first, int last) const
196 {
197 std::vector<std::string> files;
198 const std::string base{base_chars}, ext{take_ext.data()};
199 if(base.empty())
200 return files;
201 for(int k = first; k <= last; k++)
202 files.push_back(base + "-layer" + std::to_string(k + 1) + "." + ext);
203 return files;
204 }
205
207 std::vector<std::string> take_files() const
208 {
209 std::vector<std::string> files;
210 const std::string base{take_base.data()}, ext{take_ext.data()};
211 if(base.empty())
212 return files;
213 if(take_mix)
214 files.push_back(base + "." + ext);
215 auto layers = layer_files(take_base.data(), 0, take_layers - 1);
216 files.insert(files.end(), layers.begin(), layers.end());
217 return files;
218 }
219
221 std::vector<std::string> part_files() const
222 {
223 return layer_files(part_base.data(), part_first, part_last);
224 }
225};
226
228{
229 static constexpr double width() { return 360.; }
232 static constexpr double height() { return 238.; }
233 static constexpr double header = 26.;
234 static constexpr double send_w = 56.;
235
236 view_message state{};
237 bool has_state{};
238
239 std::function<void()> update;
241 std::function<void(halp::timeline_clip)> send_to_timeline;
244 std::function<void(erase_request)> erase;
245
252 int m_waiting{};
253 bool m_send_waiting{};
254
258 TakeFiles m_export{TakeFiles::Mix};
259 void watch(const auto& exported)
260 {
261 const auto files = TakeFiles(exported.value);
262 if(files == m_export)
263 return;
264 m_export = files;
265 if(update)
266 update();
267 }
268
272 int m_stopped_stops{-1};
273
278 using clock = std::chrono::steady_clock;
279 static constexpr auto silent_after = std::chrono::milliseconds(300);
280 clock::time_point m_last_message{};
281 bool live() const noexcept
282 {
283 return has_state && clock::now() - m_last_message < silent_after;
284 }
286 bool waiting() const noexcept { return m_waiting > 0 && live(); }
287
288 void receive(const view_message& m) noexcept
289 {
290 if(m.looper == m_stopped_looper && m.stops < m_stopped_stops)
291 return;
292 if(m.stopped)
293 {
294 // The looper does not run any more: nothing asked of it will come.
295 m_stopped_looper = m.looper;
296 m_stopped_stops = m.stops;
297 m_last_message = {};
298 m_waiting = 0;
299 m_send_waiting = false;
300 m_part_waiting = 0;
301 state = m;
302 has_state = true;
303 if(update)
304 update();
305 return;
306 }
307 // After a silence the looper may be another one, made when the score
308 // played again, which knows nothing of what was asked before.
309 if(!live())
310 {
311 m_waiting = 0;
312 m_send_waiting = false;
313 m_part_waiting = 0;
314 }
315 m_last_message = clock::now();
316 state = m;
317 has_state = true;
318 if(m_waiting > 0 && m.answered >= m_waiting)
319 {
320 m_waiting = 0;
321 if(std::exchange(m_send_waiting, false))
322 send_now();
323 }
324 if(m_part_waiting > 0 && m.part_answered >= m_part_waiting)
325 {
326 // Answered with the files, or with nothing when the loop changed.
327 if(m.part_answer == m_part_waiting)
328 {
329 m_part_files = m.part_files();
330 m_part_placeable = m.part_placeable;
331 m_part_stretch = m.part_stretch;
332 }
333 else
334 m_part_files.clear();
335 m_part_waiting = 0;
336 }
337 if(update)
338 update();
339 }
340
346 int m_part_lane{-1};
347 int m_part_first{-1};
348 int m_part_last{-1};
349 std::vector<std::string> m_part_files;
350 bool m_part_placeable{};
351 halp::clip_stretch m_part_stretch{};
355 mutable bool m_dragged{};
356
359 std::pair<int, int> lane_passes(int lane) const noexcept
360 {
361 const auto& s = state;
362 if(!has_state || s.empty || lane < 0 || lane >= s.lanes)
363 return {-1, -1};
364 const int merged = s.layers - s.lanes;
365 if(merged > 0 && lane == 0)
366 return {0, merged};
367 return {lane + std::max(0, merged), lane + std::max(0, merged)};
368 }
369
371 bool take_has_layers() const noexcept
372 {
373 return placeable() && !taking_in() && state.take_layers >= state.layers
374 && state.take_version == state.version && state.take_layers > 0;
375 }
376
379 std::vector<std::string> lane_files(int lane) const
380 {
381 const auto [first, last] = lane_passes(lane);
382 if(first < 0)
383 return {};
384 if(take_has_layers())
385 return state.layer_files(state.take_base.data(), first, last);
386 // Written for the press that asked for them: the next press asks again.
387 if(m_part_waiting == 0 && m_part_first == first && m_part_last == last
388 && m_part_placeable)
389 return m_part_files;
390 return {};
391 }
392
394 void request_part(int lane)
395 {
396 const auto [first, last] = lane_passes(lane);
397 if(first < 0 || !erase || !live() || m_part_waiting > 0)
398 return;
399 m_requests = std::max({m_requests, state.answered, state.part_answered}) + 1;
401 m_part_lane = lane;
402 m_part_first = first;
403 m_part_last = last;
404 m_part_files.clear();
406 .pass = first, .layers = state.layers, .mute = false, .take = m_part_waiting,
407 .last = last});
408 if(update)
409 update();
410 }
411 bool preparing(int lane) const noexcept
412 {
413 return m_part_waiting > 0 && live() && m_part_lane == lane;
414 }
415
416 bool has_take() const noexcept { return has_state && state.take > 0 && state.take_base[0]; }
418 bool placeable() const noexcept { return has_take() && state.take_placeable; }
419
421 bool taking_in() const noexcept
422 {
423 if(!live() || state.empty)
424 return false;
425 const auto m = LoopMode(state.mode);
426 return m == LoopMode::Record || m == LoopMode::Overdub;
427 }
428 bool saving() const noexcept { return waiting() || (live() && state.writing); }
429
432 bool take_has_export() const noexcept
433 {
434 return (m_export == TakeFiles::Layers || state.take_mix)
435 && (m_export == TakeFiles::Mix || state.take_layers > 0);
436 }
437
441 bool needs_take() const noexcept
442 {
443 return erase && live() && !state.empty && (taking_in() || !take_has_export());
444 }
445
448 std::vector<std::string> send_files() const
449 {
450 if(!take_has_export())
451 return state.take_files();
452 std::vector<std::string> files;
453 const std::string base{state.take_base.data()}, ext{state.take_ext.data()};
454 if(base.empty())
455 return files;
456 if(m_export != TakeFiles::Layers)
457 files.push_back(base + "." + ext);
458 if(m_export != TakeFiles::Mix)
459 {
460 auto layers = state.layer_files(state.take_base.data(), 0, state.take_layers - 1);
461 files.insert(files.end(), layers.begin(), layers.end());
462 }
463 return files;
464 }
465
468 {
469 if(waiting() || !erase)
470 return;
471 // Above what the looper answered already: another view of it may have
472 // asked before.
473 m_requests = std::max({m_requests, has_state ? state.answered : 0,
474 has_state ? state.part_answered : 0})
475 + 1;
476 m_waiting = m_requests;
477 erase(erase_request{.pass = -1, .layers = -1, .mute = false, .take = m_waiting});
478 if(update)
479 update();
480 }
481
483 void send()
484 {
485 if(needs_take())
486 {
487 m_send_waiting = true;
488 request_take();
489 return;
490 }
491 send_now();
492 }
493
494 void send_now()
495 {
496 if(!placeable() || !send_to_timeline)
497 return;
498 std::string_view base{state.take_base.data()};
499 if(auto slash = base.find_last_of("/\\"); slash != std::string_view::npos)
500 base.remove_prefix(slash + 1);
501 send_to_timeline(halp::timeline_clip{
502 .files = send_files(),
503 .date = state.take_date,
504 .name = std::string{base},
505 .stretch = state.take_stretch});
506 }
507
508 static rgba mode_color(int mode, bool empty) noexcept
509 {
510 if(empty && mode != int(LoopMode::Record))
511 return {110, 110, 116, 255};
512 switch(LoopMode(mode))
513 {
514 case LoopMode::Record:
515 return {232, 64, 60, 255};
516 case LoopMode::Overdub:
517 return {255, 150, 40, 255};
518 case LoopMode::Play:
519 return {88, 200, 112, 255};
520 case LoopMode::Stop:
521 default:
522 return {150, 150, 156, 255};
523 }
524 }
525
526 static const char* mode_name(int mode) noexcept
527 {
528 switch(LoopMode(mode))
529 {
530 case LoopMode::Record:
531 return "Record";
532 case LoopMode::Overdub:
533 return "Overdub";
534 case LoopMode::Play:
535 return "Play";
536 case LoopMode::Stop:
537 default:
538 return "Stop";
539 }
540 }
541
543 static rgba dimmed(rgba c) noexcept
544 {
545 constexpr rgba bg{34, 34, 38, 255};
546 return {
547 uint8_t((c.r + 2 * bg.r) / 3), uint8_t((c.g + 2 * bg.g) / 3),
548 uint8_t((c.b + 2 * bg.b) / 3), 255};
549 }
550
551 static rgba lane_color(int lane, int lanes) noexcept
552 {
553 static constexpr rgba palette[]{
554 {74, 160, 220, 255}, {86, 190, 170, 255}, {150, 200, 90, 255},
555 {220, 190, 70, 255}, {230, 140, 70, 255}, {210, 100, 130, 255},
556 {160, 110, 210, 255}, {110, 130, 230, 255}};
557 (void)lanes;
558 return palette[lane % 8];
559 }
560
561 void paint(auto ctx)
562 {
563 const double w = width(), h = height();
564 ctx.set_fill_color({24, 24, 26, 255});
565 ctx.begin_path();
566 ctx.draw_rect(0., 0., w, h);
567 ctx.fill();
568
569 const auto& s = state;
570 const auto col = mode_color(has_state ? s.mode : int(LoopMode::Stop), !has_state || s.empty);
571
572 // Header: progress ring, mode, what it is waiting for, length and passes.
573 {
574 const double cx = 13., cy = header / 2., r = 8.;
575 ctx.set_stroke_color({60, 60, 64, 255});
576 ctx.set_stroke_width(3.);
577 ctx.begin_path();
578 ctx.draw_circle(cx, cy, r);
579 ctx.stroke();
580 if(has_state && !s.empty)
581 {
582 ctx.set_stroke_color({col.r, col.g, col.b, col.a});
583 ctx.begin_path();
584 ctx.move_to(cx, cy - r);
585 ctx.arc_to(cx - r, cy - r, 2. * r, 2. * r, 90., -360. * s.position);
586 ctx.stroke();
587 }
588 ctx.set_stroke_width(1.);
589
590 char text[160];
591 if(!has_state)
592 std::snprintf(text, sizeof(text), "Play to start looping");
593 else if(s.stopped)
594 std::snprintf(text, sizeof(text), "Stopped with the score");
595 else if(s.wanted != s.requested)
596 std::snprintf(
597 text, sizeof(text), "%s, then %s on the grid", mode_name(s.mode),
598 mode_name(s.wanted));
599 else if(s.new_pass_pending || s.restart_pending)
600 std::snprintf(
601 text, sizeof(text), "%s, then %s on the grid", mode_name(s.mode),
602 s.new_pass_pending ? (s.restart_pending ? "new layer from the start" : "new layer")
603 : "from the start");
604 else
605 std::snprintf(
606 text, sizeof(text), "%s%s%s%s", mode_name(s.mode),
607 s.reverse ? ", reverse" : "", speed_name()[0] ? " " : "", speed_name());
608
609 ctx.set_fill_color({col.r, col.g, col.b, col.a});
610 ctx.begin_path();
611 ctx.draw_text(28., cy + 4., text);
612 ctx.fill();
613
614 if(has_state)
615 {
616 // Against the Send button, whatever its length.
617 char info[160];
618 std::snprintf(
619 info, sizeof(info), "%.1f s%s%s", s.length_seconds, s.full ? ", full" : "",
620 s.redo > 0 ? " (redo)" : "");
621 double info_w = 6.5 * std::strlen(info);
622 if constexpr(requires { ctx.text_width(std::string_view{}); })
623 info_w = ctx.text_width(info);
624 ctx.set_fill_color({170, 170, 176, 255});
625 ctx.begin_path();
626 ctx.draw_text(w - send_w - 10. - info_w, cy + 4., info);
627 ctx.fill();
628 }
629
630 // Send to timeline. A take the timeline cannot play is a file only.
631 const bool file_only = has_take() && !state.take_placeable && !taking_in();
632 const bool enabled = (placeable() || taking_in()) && bool(send_to_timeline);
633 const rgba button = enabled ? rgba{60, 110, 160, 255} : rgba{44, 44, 48, 255};
634 ctx.set_fill_color({button.r, button.g, button.b, button.a});
635 ctx.begin_path();
636 ctx.draw_rounded_rect(w - send_w - 4., 4., send_w, header - 8., 3.);
637 ctx.fill();
638 const rgba label = enabled ? rgba{235, 240, 245, 255} : rgba{110, 110, 116, 255};
639 ctx.set_fill_color({label.r, label.g, label.b, label.a});
640 // A grip: the button is also where the whole take is dragged from.
641 ctx.begin_path();
642 for(int row = 0; row < 3; row++)
643 for(int col = 0; col < 2; col++)
644 ctx.draw_rect(w - send_w + 1. + col * 3., cy - 4. + row * 3.5, 1.5, 1.5);
645 ctx.fill();
646 ctx.begin_path();
647 ctx.draw_text(
648 w - send_w + 9., cy + 4., saving() ? "Saving" : (file_only ? "File" : "Send"));
649 ctx.fill();
650 }
651
652 const double top = header, lanes_h = h - header - 4.;
653 if(!has_state || s.empty)
654 {
655 if(has_take())
656 {
657 ctx.set_fill_color({120, 120, 126, 255});
658 ctx.begin_path();
659 ctx.draw_text(10., top + lanes_h / 2., "The last take can still be sent.");
660 ctx.fill();
661 }
662 return;
663 }
664
665 if(s.midi)
666 paint_notes(ctx, top, lanes_h);
667 else if(s.values)
668 paint_values(ctx, top, lanes_h);
669 else
670 paint_lanes(ctx, top, lanes_h);
671
672 // Playhead, with an arrow the way it goes.
673 const double x = 4. + s.position * (w - 8.);
674 ctx.set_stroke_color({col.r, col.g, col.b, col.a});
675 ctx.set_stroke_width(2.);
676 ctx.begin_path();
677 ctx.move_to(x, top);
678 ctx.line_to(x, top + lane_count() * lane_height());
679 ctx.stroke();
680 ctx.set_stroke_width(1.);
681 {
682 const double dir = s.reverse ? -1. : 1.;
683 ctx.set_fill_color({col.r, col.g, col.b, col.a});
684 ctx.begin_path();
685 ctx.move_to(x, top);
686 ctx.line_to(x + dir * 7., top + 4.);
687 ctx.line_to(x, top + 8.);
688 ctx.close_path();
689 ctx.fill();
690 }
691
692 paint_erase_boxes(ctx);
693 paint_preparing(ctx);
694 }
695
697 void paint_preparing(auto& ctx)
698 {
699 if(m_part_waiting <= 0 || !live() || m_part_lane < 0 || m_part_lane >= lane_count())
700 return;
701 const double lh = lane_height();
702 const double y = lanes_top + m_part_lane * lh;
703 ctx.set_fill_color({24, 24, 26, 170});
704 ctx.begin_path();
705 ctx.draw_rect(lanes_x, y + 1., lanes_width(), lh - 2.);
706 ctx.fill();
707 ctx.set_fill_color({235, 240, 245, 255});
708 ctx.begin_path();
709 ctx.draw_text(lanes_x + 8., y + lh / 2. + 4., "Preparing");
710 ctx.fill();
711 }
712
714 const char* speed_name() const noexcept
715 {
716 for(auto& [name, value] : speeds)
717 if(value != 1.f && std::abs(value - state.speed) < 1e-4f)
718 return name;
719 return "";
720 }
721
722 // The lanes: one per pass, the oldest passes all in the first one when
723 // there are more passes than lanes.
724 static constexpr double lanes_top = header;
725 static constexpr double lanes_x = 4.;
726 static double lanes_height() noexcept { return height() - header - 4.; }
727 static double lanes_width() noexcept { return width() - 8.; }
728 int lane_count() const noexcept { return std::max(1, state.lanes); }
731 static constexpr double max_lane_height = 72.;
732 static constexpr double min_lane_height = 6.;
733 double lane_height() const noexcept
734 {
735 return std::clamp(lanes_height() / lane_count(), min_lane_height, max_lane_height);
736 }
737
739 int pass_of_lane(int lane) const noexcept
740 {
741 const auto& s = state;
742 if(!has_state || s.empty || lane < 0 || lane >= s.lanes)
743 return -1;
744 const int merged = s.layers - s.lanes;
745 if(merged > 0 && lane == 0)
746 return -1;
747 return lane + merged;
748 }
749
750 struct box
751 {
752 double x, y, size;
753 bool contains(double px, double py) const noexcept
754 {
755 return px >= x - 1. && px <= x + size + 1. && py >= y - 1. && py <= y + size + 1.;
756 }
757 };
758
760 box erase_box(int lane) const noexcept
761 {
762 const double lh = lane_height();
763 const double size = std::clamp(lh - 3., 5., 12.);
764 const double y = lanes_top + lane * lh + (lh - size) / 2.;
765 return {lanes_x + lanes_width() - size - 3., y, size};
766 }
767
769 box mute_box(int lane) const noexcept
770 {
771 const auto e = erase_box(lane);
772 return {e.x - e.size - 4., e.y, e.size};
773 }
774
775 bool lane_muted(int lane) const noexcept
776 {
777 return lane >= 0 && lane < view_message::max_lanes && state.muted[lane];
778 }
779
781 int selected_lane() const noexcept
782 {
783 const int lane = state.selected - (state.layers - state.lanes);
784 return pass_of_lane(lane) >= 0 && pass_of_lane(lane) == state.selected ? lane : -1;
785 }
786
788 static constexpr rgba lane_bg{34, 34, 38, 255};
789 static constexpr rgba selected_bg{46, 46, 54, 255};
790 rgba lane_background(int lane) const noexcept
791 {
792 return lane == selected_lane() ? selected_bg : lane_bg;
793 }
794 void outline_selected(auto& ctx, double top, double lh) const
795 {
796 const int l = selected_lane();
797 if(l < 0)
798 return;
799 ctx.set_stroke_color({190, 190, 200, 150});
800 ctx.set_stroke_width(1.);
801 ctx.begin_path();
802 ctx.draw_rect(4.5, top + l * lh + 1.5, width() - 9., lh - 3.);
803 ctx.stroke();
804 }
805
807 int erase_lane_at(double x, double y) const noexcept
808 {
809 if(!erase)
810 return -1;
811 for(int l = 0; l < lane_count(); l++)
812 if(pass_of_lane(l) >= 0 && erase_box(l).contains(x, y))
813 return l;
814 return -1;
815 }
816
818 int mute_lane_at(double x, double y) const noexcept
819 {
820 if(!erase)
821 return -1;
822 for(int l = 0; l < lane_count(); l++)
823 if(pass_of_lane(l) >= 0 && mute_box(l).contains(x, y))
824 return l;
825 return -1;
826 }
827
828 void paint_erase_boxes(auto& ctx)
829 {
830 if(!erase)
831 return;
832 for(int l = 0; l < lane_count(); l++)
833 {
834 if(pass_of_lane(l) < 0)
835 continue;
836 {
837 const auto b = mute_box(l);
838 const bool muted = lane_muted(l);
839 const rgba fill = muted ? rgba{230, 160, 60, 255} : rgba{44, 44, 50, 230};
840 const rgba ink = muted ? rgba{24, 24, 26, 255} : rgba{210, 210, 216, 255};
841 ctx.set_fill_color({fill.r, fill.g, fill.b, fill.a});
842 ctx.begin_path();
843 ctx.draw_rounded_rect(b.x, b.y, b.size, b.size, 2.);
844 ctx.fill();
845 // An m: two arches.
846 const double m = std::max(2., b.size * 0.25);
847 const double x0 = b.x + m, x1 = b.x + b.size - m, xm = (x0 + x1) / 2.;
848 const double y0 = b.y + m, y1 = b.y + b.size - m;
849 ctx.set_stroke_color({ink.r, ink.g, ink.b, ink.a});
850 ctx.set_stroke_width(1.5);
851 ctx.begin_path();
852 ctx.move_to(x0, y1);
853 ctx.line_to(x0, y0);
854 ctx.line_to(xm, (y0 + y1) / 2.);
855 ctx.line_to(x1, y0);
856 ctx.line_to(x1, y1);
857 ctx.stroke();
858 ctx.set_stroke_width(1.);
859 }
860 const auto b = erase_box(l);
861 ctx.set_fill_color({44, 44, 50, 230});
862 ctx.begin_path();
863 ctx.draw_rounded_rect(b.x, b.y, b.size, b.size, 2.);
864 ctx.fill();
865 const double m = std::max(2., b.size * 0.28);
866 ctx.set_stroke_color({210, 210, 216, 255});
867 ctx.set_stroke_width(1.5);
868 ctx.begin_path();
869 ctx.move_to(b.x + m, b.y + m);
870 ctx.line_to(b.x + b.size - m, b.y + b.size - m);
871 ctx.move_to(b.x + b.size - m, b.y + m);
872 ctx.line_to(b.x + m, b.y + b.size - m);
873 ctx.stroke();
874 ctx.set_stroke_width(1.);
875 }
876 }
877
878 void paint_lanes(auto& ctx, double top, double)
879 {
880 const auto& s = state;
881 const int lanes = std::max(1, s.lanes);
882 const double lh = lane_height();
883 const double x0 = 4., lw = width() - 8.;
884 const double bw = lw / view_message::bins;
885 for(int l = 0; l < lanes; l++)
886 {
887 const double y = top + l * lh;
888 const double mid = y + lh / 2.;
889 const auto bg = lane_background(l);
890 ctx.set_fill_color({bg.r, bg.g, bg.b, bg.a});
891 ctx.begin_path();
892 ctx.draw_rect(x0, y + 1., lw, lh - 2.);
893 ctx.fill();
894
895 auto c = lane_color(l, lanes);
896 if(lane_muted(l))
897 c = dimmed(c);
898 ctx.set_fill_color({c.r, c.g, c.b, c.a});
899 ctx.begin_path();
900 const float* p = s.peaks.data() + l * view_message::bins;
901 for(int b = 0; b < view_message::bins; b++)
902 {
903 const double a = std::min(1.f, p[b]) * (lh / 2. - 1.5);
904 if(a > 0.25)
905 ctx.draw_rect(x0 + b * bw, mid - a, std::max(1., bw - 0.5), 2. * a);
906 }
907 ctx.fill();
908 }
909 outline_selected(ctx, top, lh);
910 }
911
914 void paint_notes(auto& ctx, double top, double)
915 {
916 const auto& s = state;
917 const int lanes = std::max(1, s.lanes);
918 const double lh = lane_height();
919 const double x0 = 4., lw = width() - 8.;
920
921 std::array<int, view_message::max_lanes> lo, hi;
922 lo.fill(127);
923 hi.fill(0);
924 for(int i = 0; i < s.notes; i++)
925 {
926 const auto& n = s.note[i];
927 const int l = std::min<int>(n.lane, view_message::max_lanes - 1);
928 lo[l] = std::min<int>(lo[l], n.pitch);
929 hi[l] = std::max<int>(hi[l], n.pitch);
930 }
931
932 for(int l = 0; l < lanes; l++)
933 {
934 const double y = top + l * lh;
935 const auto lbg = lane_background(l);
936 ctx.set_fill_color({lbg.r, lbg.g, lbg.b, lbg.a});
937 ctx.begin_path();
938 ctx.draw_rect(x0, y + 1., lw, lh - 2.);
939 ctx.fill();
940 if(lo[l] > hi[l])
941 continue;
942 lo[l] = std::max(0, lo[l] - 1);
943 hi[l] = std::min(127, hi[l] + 1);
944 }
945
946 // The notes not heard first, so that the ones heard are drawn over them.
947 for(int heard = 0; heard < 2; heard++)
948 {
949 for(int i = 0; i < s.notes; i++)
950 {
951 const auto& n = s.note[i];
952 if(bool(n.heard) != bool(heard))
953 continue;
954 const int l = std::min<int>(n.lane, lanes - 1);
955 const double rh = (lh - 3.) / (hi[l] - lo[l] + 1);
956 const double y = top + l * lh + 1.5 + (hi[l] - n.pitch) * rh;
957 const double nh = std::clamp(rh - 0.5, 2., 6.);
958 auto c = lane_color(l, lanes);
959 if(!heard)
960 c = dimmed(c);
961 ctx.set_fill_color({c.r, c.g, c.b, c.a});
962 ctx.begin_path();
963 const double a = x0 + n.start * lw;
964 const double yc = std::round(y + (rh - nh) / 2.);
965 if(n.end >= n.start)
966 ctx.draw_rect(a, yc, std::max(2., (n.end - n.start) * lw), nh);
967 else
968 {
969 ctx.draw_rect(a, yc, std::max(2., (1. - n.start) * lw), nh);
970 ctx.draw_rect(x0, yc, std::max(2., n.end * lw), nh);
971 }
972 ctx.fill();
973 }
974 }
975 outline_selected(ctx, top, lh);
976 }
977
982 void paint_values(auto& ctx, double top, double)
983 {
984 const auto& s = state;
985 const int lanes = std::max(1, s.lanes);
986 const double lh = lane_height();
987 const double x0 = 4., lw = width() - 8.;
988 const double bw = lw / view_message::bins;
989 for(int l = 0; l < lanes; l++)
990 {
991 const double y = top + l * lh;
992 const auto bg = lane_background(l);
993 ctx.set_fill_color({bg.r, bg.g, bg.b, bg.a});
994 ctx.begin_path();
995 ctx.draw_rect(x0, y + 1., lw, lh - 2.);
996 ctx.fill();
997
998 const float* p = s.peaks.data() + l * view_message::bins;
999 const uint8_t* f = s.value_flags.data() + l * view_message::bins;
1000 const auto base = lane_color(l, lanes);
1001 const auto quiet = dimmed(base);
1002 const double inner = std::max(2., lh - 8.);
1003 auto height = [&](int b) {
1004 if(f[b] & view_message::flag_number)
1005 return y + 4. + (1. - std::clamp(double(p[b]), 0., 1.)) * inner;
1006 return y + lh / 2.;
1007 };
1008 // The heard parts over the others.
1009 for(int heard = 0; heard < 2; heard++)
1010 {
1011 auto c = heard ? base : quiet;
1012 if(lane_muted(l))
1013 c = dimmed(c);
1014 ctx.set_stroke_color({c.r, c.g, c.b, c.a});
1015 ctx.set_stroke_width(heard ? 1.5 : 1.);
1016 ctx.begin_path();
1017 for(int b = 0; b < view_message::bins; b++)
1018 {
1019 const bool held = f[b] & (view_message::flag_number | view_message::flag_other);
1020 if(!held || bool(f[b] & view_message::flag_heard) != bool(heard))
1021 continue;
1022 const double xa = x0 + b * bw, ya = height(b);
1023 ctx.move_to(xa, ya);
1024 ctx.line_to(xa + bw, ya);
1025 // The step to the next bin's value.
1026 if(b + 1 < view_message::bins
1028 {
1029 const double yb = height(b + 1);
1030 if(std::abs(yb - ya) > 0.5)
1031 {
1032 ctx.move_to(xa + bw, ya);
1033 ctx.line_to(xa + bw, yb);
1034 }
1035 }
1036 }
1037 ctx.stroke();
1038 }
1039 ctx.set_stroke_width(1.);
1040
1041 // Impulses and values that are not numbers: ticks.
1042 for(int b = 0; b < view_message::bins; b++)
1043 {
1045 continue;
1046 const bool heard = (f[b] & view_message::flag_heard_mark)
1047 || ((f[b] & view_message::flag_mark) && !lane_muted(l));
1048 auto c = heard ? base : quiet;
1049 ctx.set_fill_color({c.r, c.g, c.b, c.a});
1050 ctx.begin_path();
1051 const double xa = x0 + b * bw;
1053 ctx.draw_rect(xa, y + 2., std::max(1.5, bw), lh - 4.);
1054 else
1055 ctx.draw_rect(xa, y + lh / 2. - 3., std::max(2., bw), 6.);
1056 ctx.fill();
1057 }
1058 }
1059 outline_selected(ctx, top, lh);
1060 }
1061
1062 bool in_send(double x, double y) const noexcept
1063 {
1064 return x >= width() - send_w - 4. && x <= width() - 4. && y >= 4. && y <= header - 4.;
1065 }
1066
1068 int lane_at(double x, double y) const noexcept
1069 {
1070 if(!has_state || state.empty || x < lanes_x || x > lanes_x + lanes_width())
1071 return -1;
1072 const double lh = lane_height();
1073 if(y < lanes_top || y >= lanes_top + lane_count() * lh)
1074 return -1;
1075 return std::min(lane_count() - 1, int((y - lanes_top) / lh));
1076 }
1077
1078 bool mouse_press(double x, double y)
1079 {
1080 m_dragged = false;
1081 m_button_pressed = false;
1082 if(m_part_waiting == 0)
1083 {
1084 m_part_files.clear();
1085 m_part_first = m_part_last = -1;
1086 }
1087 if(in_send(x, y))
1088 {
1089 // The take is asked for at once: a drag waits for it, a click sends it.
1090 m_button_pressed = true;
1091 if(needs_take())
1092 request_take();
1093 return true;
1094 }
1095 if(const int lane = erase_lane_at(x, y); lane >= 0)
1096 {
1097 erase(erase_request{.pass = pass_of_lane(lane), .layers = state.layers, .mute = false});
1098 return true;
1099 }
1100 if(const int lane = mute_lane_at(x, y); lane >= 0)
1101 {
1102 erase(erase_request{.pass = pass_of_lane(lane), .layers = state.layers, .mute = true});
1103 return true;
1104 }
1105 // A press on a lane is the start of a drag of its passes.
1106 const int lane = lane_at(x, y);
1107 if(lane < 0)
1108 return false;
1109 if(!lane_files(lane).empty())
1110 return true;
1111 if(live() && erase)
1112 {
1113 request_part(lane);
1114 return true;
1115 }
1116 return false;
1117 }
1118 void mouse_move(double, double) { }
1119 void mouse_release(double x, double y)
1120 {
1121 if(std::exchange(m_button_pressed, false) && !m_dragged && in_send(x, y))
1122 send();
1123 }
1124
1125 halp::drag_payload drag_payload(double x, double y) const
1126 {
1127 halp::drag_payload p;
1128 halp::clip_stretch stretch = state.take_stretch;
1129 if(in_send(x, y))
1130 {
1131 // The take, as Send places it.
1132 if(waiting() || !placeable())
1133 return p;
1134 p.files = send_files();
1135 }
1136 else if(const int lane = lane_at(x, y);
1137 lane >= 0 && erase_lane_at(x, y) < 0 && mute_lane_at(x, y) < 0)
1138 {
1139 if(preparing(lane))
1140 return p;
1141 p.files = lane_files(lane);
1142 if(!take_has_layers())
1143 stretch = m_part_stretch;
1144 }
1145 if(p.files.empty())
1146 return p;
1147 m_dragged = true;
1148 // A take is a loop wherever it is dropped.
1149 p.data = Process::encode_clip_metadata(p.files, stretch, true);
1150 if(!p.data.empty())
1152 return p;
1153 }
1154};
1155
1158template <typename Inputs>
1159struct looper_view_item : halp::custom_actions_item<looper_view>
1160{
1161 static constexpr auto watched() { return std::tuple{&Inputs::exported}; }
1162};
1163
1164static_assert(
1165 view_message::max_lanes * looper_view::min_lane_height
1166 <= looper_view::height() - looper_view::header - 4.);
1167}
@ Stop
The end state, sent at stop.
Definition AudioLayers.hpp:14
TakeFiles
Definition LooperView.hpp:46
constexpr std::array< std::pair< const char *, float >, 5 > speeds
The speeds a loop is played at, as a tape: the pitch follows.
Definition score-plugin-fx/Fx/Looper/Transport.hpp:161
constexpr std::string_view clip_metadata_mime
Definition ClipMetadata.hpp:19
std::string encode_clip_metadata(const std::vector< std::string > &files, halp::clip_stretch stretch, bool loop=false)
Empty when there is nothing to say: every file is left to the host.
Definition ClipMetadata.hpp:49
Definition LooperView.hpp:61
Definition LooperView.hpp:751
Definition LooperView.hpp:1160
Definition LooperView.hpp:228
TakeFiles m_export
Definition LooperView.hpp:258
std::chrono::steady_clock clock
Definition LooperView.hpp:278
int m_requests
Definition LooperView.hpp:251
bool take_has_export() const noexcept
Definition LooperView.hpp:432
std::pair< int, int > lane_passes(int lane) const noexcept
Definition LooperView.hpp:359
int erase_lane_at(double x, double y) const noexcept
The lane whose erase button is at (x, y), -1 for none.
Definition LooperView.hpp:807
box erase_box(int lane) const noexcept
The erase button of a lane, at its right edge.
Definition LooperView.hpp:760
void request_part(int lane)
Asks for the files of a lane's passes, unless they are being asked for.
Definition LooperView.hpp:394
bool placeable() const noexcept
The last take can go in the timeline.
Definition LooperView.hpp:418
int pass_of_lane(int lane) const noexcept
The pass a lane shows alone, -1 for a lane of several passes or none.
Definition LooperView.hpp:739
void paint_values(auto &ctx, double top, double)
Definition LooperView.hpp:982
bool take_has_layers() const noexcept
The last take has the passes as they are, each in a file.
Definition LooperView.hpp:371
std::function< void(halp::timeline_clip)> send_to_timeline
Set by the host when it can place clips in the timeline.
Definition LooperView.hpp:241
void paint_preparing(auto &ctx)
The lane whose files are being written for a drag.
Definition LooperView.hpp:697
void paint_notes(auto &ctx, double top, double)
Definition LooperView.hpp:914
int m_part_waiting
Definition LooperView.hpp:345
bool m_button_pressed
Definition LooperView.hpp:354
std::function< void(erase_request)> erase
Definition LooperView.hpp:244
int lane_at(double x, double y) const noexcept
The lane at (x, y), -1 for none.
Definition LooperView.hpp:1068
void request_take()
Asks for a take of the loop as it is now, unless one was asked already.
Definition LooperView.hpp:467
int mute_lane_at(double x, double y) const noexcept
The lane whose mute button is at (x, y), -1 for none.
Definition LooperView.hpp:818
static constexpr rgba lane_bg
The selected lane: a lighter background, and an outline.
Definition LooperView.hpp:788
static rgba dimmed(rgba c) noexcept
What is not heard: a third of its colour over the background.
Definition LooperView.hpp:543
box mute_box(int lane) const noexcept
The mute button of a lane, left of its erase button.
Definition LooperView.hpp:769
int selected_lane() const noexcept
The lane of the pass Layer says, when it has one of its own, else -1.
Definition LooperView.hpp:781
static constexpr double max_lane_height
Definition LooperView.hpp:731
std::vector< std::string > send_files() const
Definition LooperView.hpp:448
bool taking_in() const noexcept
A pass is being taken in: the last take does not have it.
Definition LooperView.hpp:421
void send()
The take in one box named after it, a slot per file, the mix first.
Definition LooperView.hpp:483
std::vector< std::string > lane_files(int lane) const
Definition LooperView.hpp:379
bool waiting() const noexcept
A take asked for is still awaited from a looper that runs.
Definition LooperView.hpp:286
const char * speed_name() const noexcept
The speed the loop plays at, empty at x1.
Definition LooperView.hpp:714
uint64_t m_stopped_looper
Definition LooperView.hpp:271
bool needs_take() const noexcept
Definition LooperView.hpp:441
static constexpr double height()
Definition LooperView.hpp:232
A colour, handed to the painter as {r, g, b, a}.
Definition LooperView.hpp:27
Definition LooperView.hpp:72
std::array< char, 512 > part_base
Definition LooperView.hpp:148
double take_date
Where the last take was recorded, in seconds from the process' start.
Definition LooperView.hpp:101
bool take_placeable
Definition LooperView.hpp:122
int selected
The pass Layer says, from the oldest one, which the layer actions act on.
Definition LooperView.hpp:132
bool reverse
The loop plays backwards, and at this speed.
Definition LooperView.hpp:127
std::vector< std::string > layer_files(const char *base_chars, int first, int last) const
UI thread: the files of passes first to last of a take of base.
Definition LooperView.hpp:195
int answered
Definition LooperView.hpp:139
std::array< char, 512 > take_base
Definition LooperView.hpp:116
std::vector< std::string > part_files() const
UI thread: the files of the part last written.
Definition LooperView.hpp:221
std::vector< std::string > take_files() const
UI thread: the paths of the last take's files, the mix first.
Definition LooperView.hpp:207
bool restart_pending
Definition LooperView.hpp:135
std::array< uint8_t, max_lanes > muted
Lanes of a single pass that is muted.
Definition LooperView.hpp:130
int take
Takes written so far; the files of the last one are take_files().
Definition LooperView.hpp:86
bool values
Definition LooperView.hpp:95
value_flag
Definition LooperView.hpp:105
@ flag_number
A number is held, at the height in peaks.
Definition LooperView.hpp:106
@ flag_impulse
An impulse fires in the bin.
Definition LooperView.hpp:109
@ flag_heard_mark
The impulse is heard.
Definition LooperView.hpp:111
@ flag_mark
A value that is not a number starts in the bin.
Definition LooperView.hpp:110
@ flag_heard
What is held is what the loop plays there.
Definition LooperView.hpp:108
@ flag_other
Something else is held: text, a list, a map.
Definition LooperView.hpp:107
int mode
Definition LooperView.hpp:79
halp::clip_stretch take_stretch
Definition LooperView.hpp:125
int version
Definition LooperView.hpp:142
bool stopped
Definition LooperView.hpp:159
Definition LooperView.hpp:32
float start
Start and end in the loop, from 0 to 1. An end before the start wraps.
Definition LooperView.hpp:34
uint8_t heard
Zero when the note is not heard, e.g. replaced by a later pass.
Definition LooperView.hpp:40