Zrythm v2.0.0-DEV
a highly automated and intuitive digital audio workstation
Loading...
Searching...
No Matches
arranger_object_span.h
1// SPDX-FileCopyrightText: © 2025 Alexandros Theodotou <alex@zrythm.org>
2// SPDX-License-Identifier: LicenseRef-ZrythmLicense
3
4#pragma once
5
6#include <cstdint>
7
8#include "structure/arrangement/arranger_object_all.h"
9#include "utils/uuid_identifiable_object.h"
10
11namespace zrythm::structure::arrangement
12{
13
17enum class ResizeType : std::uint8_t
18{
19 Normal,
20 Loop,
21 Fade,
22 Stretch,
28 StretchTempoChange,
29};
30
40template <BoundedObject ObjectT>
41void
42resize_bounded_object (ObjectT &obj, bool left, ResizeType type, double ticks)
43{
44 z_trace ("resizing object( left: {}, type: {}, ticks: {})", left, type, ticks);
45
46 // fade resizes are handled directly (not via this function)
47
48 if (left)
49 {
50 if (type != ResizeType::Fade)
51 {
52 obj.getPosition ()->setTicks (obj.getPosition ()->ticks () + ticks);
53
54 if constexpr (FadeableObject<ObjectT>)
55 {
56 obj.get_fade_range ().startOffset->setTicks (
57 obj.get_fade_range ().startOffset->ticks () - ticks);
58 }
59
60 if (type == ResizeType::Loop)
61 {
62 if constexpr (RegionObject<ObjectT>)
63 {
64 double loop_len =
65 obj.get_loop_range ().get_loop_length_in_ticks ();
66
67 // if clip start is not before loop start, adjust clip start pos
68 const auto loop_start_pos =
69 obj.get_loop_range ().loopStartPosition ()->ticks ();
70 auto clip_start_pos =
71 obj.get_loop_range ().clipStartPosition ()->ticks ();
72 if (clip_start_pos >= loop_start_pos)
73 {
74 clip_start_pos += ticks;
75
76 while (clip_start_pos < loop_start_pos)
77 {
78 clip_start_pos += loop_len;
79 }
80 obj.get_loop_range ().clipStartPosition ()->setTicks (
81 clip_start_pos);
82 }
83
84 /* make sure clip start goes back to loop start if it exceeds
85 * loop end */
86 const auto loop_end_pos =
87 obj.get_loop_range ().loopEndPosition ()->ticks ();
88 while (clip_start_pos > loop_end_pos)
89 {
90 clip_start_pos += -loop_len;
91 }
92 obj.get_loop_range ().clipStartPosition ()->setTicks (
93 clip_start_pos);
94 }
95 }
96 else if constexpr (RegionObject<ObjectT>)
97 {
98 obj.get_loop_range ().loopEndPosition ()->setTicks (
99 obj.get_loop_range ().loopEndPosition ()->ticks () - ticks);
100
101 /* move containing items */
102 if constexpr (
103 is_derived_from_template_v<ArrangerObjectOwner, ObjectT>)
104 {
105 obj.add_ticks_to_children (-ticks);
106 }
107 }
108 }
109 }
110 /* else if resizing right side */
111 else
112 {
113 if (type != ResizeType::Fade)
114 {
115 obj.length ()->setTicks (obj.length ()->ticks () + ticks);
116
117 const auto change_ratio =
118 (obj.length ()->ticks ()) / (obj.length ()->ticks () - ticks);
119
120 if (type != ResizeType::Loop)
121 {
122 if constexpr (RegionObject<ObjectT>)
123 {
124 if (
125 type == ResizeType::StretchTempoChange
126 || type == ResizeType::Stretch)
127 {
128 obj.get_loop_range ().loopEndPosition ()->setTicks (
129 obj.get_loop_range ().loopEndPosition ()->ticks ()
130 * change_ratio);
131 }
132 else
133 {
134 obj.get_loop_range ().loopEndPosition ()->setTicks (
135 obj.get_loop_range ().loopEndPosition ()->ticks ()
136 + ticks);
137 }
138
139 /* if stretching, also stretch loop start */
140 if (
141 type == ResizeType::StretchTempoChange
142 || type == ResizeType::Stretch)
143 {
144 obj.get_loop_range ().loopStartPosition ()->setTicks (
145 obj.get_loop_range ().loopStartPosition ()->ticks ()
146 * change_ratio);
147 }
148 }
149 }
150 if constexpr (FadeableObject<ObjectT>)
151 {
152 // we only need changes when stretching
153 if (
154 type == ResizeType::StretchTempoChange
155 || type == ResizeType::Stretch)
156 {
157 obj.get_fade_range ().endOffset ()->setTicks (
158 obj.get_fade_range ().endOffset ()->ticks () * change_ratio);
159 obj.get_fade_range ().startOffset ()->setTicks (
160 obj.get_fade_range ().startOffset ()->ticks ()
161 * change_ratio);
162 }
163 }
164
165// TODO
166#if 0
167 if (type == ResizeType::Stretch)
168 {
169 if constexpr (std::derived_from<ObjT, Region>)
170 {
171 double new_length = get_length_in_ticks ();
172
173 if (type != ResizeType::StretchTempoChange)
174 {
175 /* FIXME this flag is not good,
176 * remove from this function and
177 * do it in the arranger */
178 if (during_ui_action)
179 {
180 self->stretch_ratio_ =
181 new_length / self->before_length_;
182 }
183 /* else if as part of an action */
184 else
185 {
186 /* stretch contents */
187 double stretch_ratio = new_length / before_length;
188 try
189 {
190 self->stretch (stretch_ratio);
191 }
192 catch (const ZrythmException &ex)
193 {
194 throw ZrythmException (
195 "Failed to stretch region");
196 }
197 }
198 }
199 }
200 }
201#endif
202 }
203 }
204}
205
215template <RegionObject RegionT>
216void
217stretch_region_contents (RegionT &r, double ratio)
218{
219 z_debug ("stretching region {} (ratio {:f})", r, ratio);
220
221// TODO
222#if 0
223 if constexpr (std::is_same_v<RegionT, AudioRegion>)
224 {
225 auto * clip = r.get_clip ();
226 auto new_clip_id = AUDIO_POOL->duplicate_clip (clip->get_uuid (), false);
227 auto * new_clip = AUDIO_POOL->get_clip (new_clip_id);
228 r.set_clip_id (new_clip->get_uuid ());
229
230 auto stretcher = dsp::Stretcher::create_rubberband (
231 AUDIO_ENGINE->get_sample_rate (), new_clip->get_num_channels (), ratio,
232 1.0, false);
233
234 auto buf = new_clip->get_samples ();
235 buf.interleave_samples ();
236 auto stretched_buf = stretcher->stretch_interleaved (buf);
237 stretched_buf.deinterleave_samples (new_clip->get_num_channels ());
238 new_clip->clear_frames ();
239 new_clip->expand_with_frames (stretched_buf);
240 auto num_frames_per_channel = new_clip->get_num_frames ();
241 z_return_if_fail (num_frames_per_channel > 0);
242
243 AUDIO_POOL->write_clip (*new_clip, false, false);
244
245 /* readjust end position to match the number of frames exactly */
246 dsp::Position new_end_pos (
247 static_cast<int64_t> (num_frames_per_channel),
248 AUDIO_ENGINE->ticks_per_frame_);
249
250 r.loopEndPosition ()->setSamples (num_frames_per_channel);
251 r.length ()->setSamples (num_frames_per_channel);
252 }
253 else
254 {
255 auto objs = r.get_children_view ();
256 for (auto * obj : objs)
257 {
258 using ObjT = base_type<decltype (obj)>;
259 /* set start pos */
260 double before_ticks = obj->get_position ().ticks_;
261 double new_ticks = before_ticks * ratio;
262 dsp::Position tmp (new_ticks, AUDIO_ENGINE->frames_per_tick_);
263 obj->position_setter_validated (tmp, AUDIO_ENGINE->ticks_per_frame_);
264
265 if constexpr (std::derived_from<ObjT, BoundedObject>)
266 {
267 /* set end pos */
268 before_ticks = obj->end_pos_->ticks_;
269 new_ticks = before_ticks * ratio;
270 tmp = dsp::Position (new_ticks, AUDIO_ENGINE->frames_per_tick_);
271 obj->end_position_setter_validated (
272 tmp, AUDIO_ENGINE->ticks_per_frame_);
273 }
274 }
275 }
276#endif
277}
278
283 : public utils::UuidIdentifiableObjectView<ArrangerObjectRegistry>
284{
285public:
287 using VariantType = typename Base::VariantType;
288 using ArrangerObjectUuid = typename Base::UuidType;
289 using Base::Base; // Inherit constructors
290
291 static auto name_projection (const VariantType &obj_var)
292 {
293 return std::visit (
294 [] (const auto &obj) -> utils::Utf8String {
295 using ObjT = base_type<decltype (obj)>;
296 if constexpr (NamedObject<ObjT>)
297 {
298 if constexpr (RegionObject<ObjT>)
299 {
300 return obj->name ()->get_name ();
301 }
302 else if constexpr (std::is_same_v<ObjT, Marker>)
303 {
304 return obj->name ()->get_name ();
305 }
306 }
307 else
308 {
309 throw std::runtime_error (
310 "Name projection called on non-named object");
311 }
312 },
313 obj_var);
314 }
315 static auto position_ticks_projection (const VariantType &obj_var)
316 {
317 return std::visit (
318 [&] (auto &&obj) { return obj->position ()->ticks (); }, obj_var);
319 }
320 static auto end_position_ticks_with_start_position_fallback_projection (
321 const VariantType &obj_var)
322 {
323 return std::visit (
324 [&] (auto &&obj) {
325 using ObjT = base_type<decltype (obj)>;
326 auto ticks = obj->position ()->ticks ();
327 if constexpr (BoundedObject<ObjT>)
328 {
329 if constexpr (RegionObject<ObjT>)
330 {
331 return ticks + obj->bounds ()->length ()->ticks ();
332 }
333 else
334 {
335 return ticks + obj->bounds ()->length ()->ticks ();
336 }
337 }
338 else
339 {
340 return ticks;
341 }
342 },
343 obj_var);
344 }
345 static auto midi_note_pitch_projection (const VariantType &obj_var)
346 {
347 return std::get<MidiNote *> (obj_var)->pitch ();
348 }
349 static auto looped_projection (const VariantType &obj_var)
350 {
351 return std::visit (
352 [] (const auto &obj) {
353 using ObjT = base_type<decltype (obj)>;
354 if constexpr (RegionObject<ObjT>)
355 {
356 return obj->loopRange ()->looped ();
357 }
358 else
359 return false;
360 },
361 obj_var);
362 }
363 static auto is_timeline_object_projection (const VariantType &obj_var)
364 {
365 return std::visit (
366 [] (const auto &ptr) { return TimelineObject<base_type<decltype (ptr)>>; },
367 obj_var);
368 }
369 static auto is_editor_object_projection (const VariantType &obj_var)
370 {
371 return !is_timeline_object_projection (obj_var);
372 }
373 static auto deletable_projection (const VariantType &obj_var)
374 {
375 return std::visit (
376 [&] (auto &&obj) { return is_arranger_object_deletable (*obj); }, obj_var);
377 }
378 static auto cloneable_projection (const VariantType &obj_var)
379 {
380 return deletable_projection (obj_var);
381 }
382 static auto renameable_projection (const VariantType &obj_var)
383 {
384 return std::visit (
385 [] (const auto &ptr) {
386 return NamedObject<base_type<decltype (ptr)>>;
387 },
388 obj_var)
389 && deletable_projection (obj_var);
390 }
391 static auto bounded_projection (const VariantType &obj_var)
392 {
393 return std::visit (
394 [] (const auto &ptr) { return BoundedObject<base_type<decltype (ptr)>>; },
395 obj_var);
396 }
397 static auto is_region_projection (const VariantType &obj_var)
398 {
399 return std::visit (
400 [] (const auto &ptr) { return RegionObject<base_type<decltype (ptr)>>; },
401 obj_var);
402 }
403 static auto bounds_projection (const VariantType &obj_var)
404 {
405 return std::visit (
406 [] (const auto &ptr) -> ArrangerObjectBounds * {
407 using ObjT = base_type<decltype (ptr)>;
408 if constexpr (BoundedObject<ObjT>)
409 {
410 return get_object_bounds (*ptr);
411 }
412 else
413 {
414 throw std::runtime_error ("Not a bounded object");
415 }
416 },
417 obj_var);
418 }
419
420// deprecated - no snapshots - use to/from json
421#if 0
422 std::vector<VariantType>
423 create_snapshots (const auto &object_factory, QObject &owner) const
424 {
425 return std::ranges::to<std::vector> (
426 *this | std::views::transform ([&] (const auto &obj_var) {
427 return std::visit (
428 [&] (auto &&obj) -> VariantType {
429 return object_factory.clone_object_snapshot (*obj, owner);
430 },
431 obj_var);
432 }));
433 }
434#endif
435
436 auto create_new_identities (const auto &object_factory) const
437 -> std::vector<ArrangerObjectUuidReference>
438 {
439 return std::ranges::to<std::vector> (
440 *this | std::views::transform ([&] (const auto &obj_var) {
441 return std::visit (
442 [&] (auto &&obj) -> VariantType {
443 return object_factory.clone_new_object_identity (*obj);
444 },
445 obj_var);
446 }));
447 }
448
457 std::vector<VariantType> sort_by_indices (bool desc);
458
459 // std::vector<VariantType> sort_by_positions (bool desc);
460
465 auto get_first_object_and_pos () const -> std::pair<VariantType, double>;
466
474 auto get_last_object_and_pos (bool ends_last) const
475 -> std::pair<VariantType, double>;
476
477 std::pair<MidiNote *, MidiNote *> get_first_and_last_note () const
478 {
479 auto midi_notes =
480 *this
481 | std::views::transform (Base::template type_transformation<MidiNote>);
482 auto [min_it, max_it] =
483 std::ranges::minmax_element (midi_notes, {}, &MidiNote::pitch);
484 return { *min_it, *max_it };
485 }
486
492 {
493 return !std::ranges::all_of (*this, deletable_projection);
494 }
495
501 {
502 return !std::ranges::all_of (*this, cloneable_projection);
503 }
504
508 {
509 return !std::ranges::all_of (*this, renameable_projection);
510 }
511
518 // bool contains_object_with_property (Property property, bool value) const;
519
525 auto merge () const -> ArrangerObjectUuidReference;
526
530 bool can_be_pasted () const;
531
532 bool all_on_same_lane () const;
533
534 bool contains_looped () const
535 {
536 return std::ranges::any_of (*this, looped_projection);
537 };
538
539 bool can_be_merged () const;
540
541 double get_length_in_ticks () const
542 {
543 auto [_1, p1] = get_first_object_and_pos ();
544 auto [_2, p2] = get_last_object_and_pos (true);
545 return p2 - p1;
546 }
547
554 std::optional<VariantType> get_bounded_object_at_position (
555 units::sample_t pos_samples,
556 bool include_region_end = false) const
557 {
558 auto view = *this | std::views::filter (bounded_projection);
559 auto it = std::ranges::find_if (view, [&] (const auto &r_var) {
560 auto bounds = bounds_projection (r_var);
561 return bounds->is_hit (pos_samples, include_region_end);
562 });
563 return it != view.end () ? std::make_optional (*it) : std::nullopt;
564 }
565
566// TODO: implement elsewhere
567#if 0
572 void unlisten_note_diff (const ArrangerObjectSpan &prev) const
573 {
574 // Check for notes in prev that are not in objects_ and stop listening to
575 // them
576 for (const auto &prev_mn : prev.template get_elements_by_type<MidiNote> ())
577 {
578 if (std::ranges::none_of (*this, [&prev_mn] (const auto &obj) {
579 auto mn = std::get<MidiNote *> (obj);
580 return *mn == *prev_mn;
581 }))
582 {
583 prev_mn->listen (false);
584 }
585 }
586 }
587#endif
588
589 // void sort_by_pitch (bool desc);
590
601 template <BoundedObject BoundedObjectT>
603 const BoundedObjectT &self,
604 const auto &factory,
605 int64_t global_pos)
606 -> std::pair<ArrangerObjectUuidReference, ArrangerObjectUuidReference>
607 {
608 const auto &tempo_map = self.get_tempo_map ();
609 /* create the new objects as new identities */
610 auto new_object1_ref = factory.clone_new_object_identity (self);
611 auto new_object2_ref = factory.clone_new_object_identity (self);
612 const auto get_derived_object = [] (auto &obj_ref) {
613 return std::get<BoundedObjectT *> (obj_ref.get_object ());
614 };
615
616 z_debug ("splitting objects...");
617
618 /* get global/local positions (the local pos is after traversing the
619 * loops) */
620 auto local_pos = [&] () {
621 auto local_frames = global_pos;
622 if constexpr (TimelineObject<BoundedObjectT>)
623 {
624 local_frames = timeline_frames_to_local (self, global_pos, true);
625 }
626 return local_frames;
627 }();
628
629 /*
630 * for first object set:
631 * - end pos (fade out position follows it)
632 */
633 ArrangerObjectSpan::bounds_projection (get_derived_object (new_object1_ref))
634 ->length ()
635 ->setSamples (
636 global_pos
637 - get_derived_object (new_object1_ref)->position ()->samples ());
638
639 if constexpr (RegionObject<BoundedObjectT>)
640 {
641 /* if original object was not looped, make the new object unlooped
642 * also */
643 if (!self.loopRange ()->is_looped ())
644 {
645 if constexpr (std::is_same_v<BoundedObjectT, AudioRegion>)
646 {
647// TODO
648#if 0
649 /* create new audio region */
650 auto prev_r1_ref = new_object1_ref;
651 auto prev_r1_clip =
652 get_derived_object (prev_r1_ref)->get_clip ();
653 assert (prev_r1_clip);
655 prev_r1_clip->get_num_channels (),
656 static_cast<int> (local_pos.frames_)
657 };
658 for (int i = 0; i < prev_r1_clip->get_num_channels (); ++i)
659 {
660 frames.copyFrom (
661 i, 0, prev_r1_clip->get_samples (), i, 0,
662 static_cast<int> (local_pos.frames_));
663 }
664 assert (!get_derived_object (prev_r1_ref)->get_name ().empty ());
665 assert (local_pos.frames_ >= 0);
666 new_object1_ref =
667 factory.create_audio_region_from_audio_buffer_FIXME (
668 get_derived_object (prev_r1_ref)->get_lane (), frames,
669 prev_r1_clip->get_bit_depth (),
670 get_derived_object (prev_r1_ref)->get_name (),
671 get_derived_object (prev_r1_ref)->get_position ().ticks_);
672 assert (
673 get_derived_object (new_object1_ref)->get_clip_id ()
674 != get_derived_object (prev_r1_ref)->get_clip_id ());
675#endif
676 }
677 else
678 {
679 /* remove objects starting after the end */
680 for (
681 const auto * child :
682 get_derived_object (new_object1_ref)->get_children_view ())
683 {
684 if (child->position ()->samples () > local_pos)
685 {
686 get_derived_object (new_object1_ref)
687 ->remove_object (child->get_uuid ());
688 }
689 }
690 }
691 }
692 }
693
694 /*
695 * for second object set:
696 * - start pos
697 * - clip start pos
698 */
699 if constexpr (RegionObject<BoundedObjectT>)
700 {
701 get_derived_object (new_object2_ref)
702 ->loopRange ()
703 ->clipStartPosition ()
704 ->setSamples (local_pos);
705 }
706 get_derived_object (new_object2_ref)->position ()->setSamples (global_pos);
707 int64_t r2_local_end =
708 ArrangerObjectSpan::bounds_projection (get_derived_object (new_object2_ref))
709 ->get_end_position_samples (true);
710 r2_local_end -=
711 get_derived_object (new_object2_ref)->position ()->samples ();
712
713 /* if original object was not looped, make the new object unlooped also */
714 if constexpr (RegionObject<BoundedObjectT>)
715 {
716 if (!self.loopRange ()->is_looped ())
717 {
718 get_derived_object (new_object2_ref)
719 ->loopRange ()
720 ->setTrackBounds (true);
721
722 /* if audio region, create a new region */
723 if constexpr (std::is_same_v<BoundedObjectT, AudioRegion>)
724 {
725// TODO
726#if 0
727 auto prev_r2_ref = new_object2_ref;
728 auto prev_r2_clip =
729 get_derived_object (prev_r2_ref)->get_clip ();
730 assert (prev_r2_clip);
731 assert (r2_local_end > 0);
733 prev_r2_clip->get_num_channels (), (int) r2_local_end
734 };
735 for (int i = 0; i < prev_r2_clip->get_num_channels (); ++i)
736 {
737 tmp.copyFrom (
738 i, 0, prev_r2_clip->get_samples (), i, local_pos,
739 r2_local_end);
740 }
741 assert (!get_derived_object (prev_r2_ref)->get_name ().empty ());
742 assert (r2_local_end >= 0);
743 new_object2_ref =
744 factory.create_audio_region_from_audio_buffer_FIXME (
745 get_derived_object (prev_r2_ref)->get_lane (), tmp,
746 prev_r2_clip->get_bit_depth (),
747 get_derived_object (prev_r2_ref)->get_name (), local_pos);
748 assert (
749 get_derived_object (new_object2_ref)->get_clip_id ()
750 != get_derived_object (prev_r2_ref)->get_clip_id ());
751#endif
752 }
753 else
754 {
755 /* move all objects backwards */
756 const double ticks_to_subtract =
757 tempo_map.samples_to_tick (units::samples (local_pos))
758 .in (units::ticks);
759 get_derived_object (new_object2_ref)
760 ->add_ticks_to_children (-ticks_to_subtract);
761
762/* remove objects starting before the start */
763// TODO
764#if 0
765 for (
766 auto * child :
767 get_derived_object (new_object2_ref)->get_children_view ())
768 {
769 if (child->position ().frames_ < 0)
770 get_derived_object (new_object2_ref)
771 ->remove_object (child->get_uuid ());
772 }
773#endif
774 }
775 }
776 }
777
778 /* make sure regions have names */
779// TODO
780#if 0
781 if constexpr (RegionObject<BoundedObjectT>)
782 {
783 auto track_var = self.get_track ();
784 std::visit (
785 [&] (auto &&track) {
787 if constexpr (std::is_same_v<BoundedObjectT, AutomationRegion>)
788 {
789 at = self.get_automation_track ();
790 }
791 get_derived_object (new_object1_ref)
792 ->generate_name (self.get_name (), at, track);
793 get_derived_object (new_object2_ref)
794 ->generate_name (self.get_name (), at, track);
795 },
796 track_var);
797 }
798#endif
799
800 return std::make_pair (new_object1_ref, new_object2_ref);
801 }
802
811// deprecated - use from/to json
812#if 0
813 static void copy_arranger_object_identifier (
814 const VariantType &dest,
815 const VariantType &src);
816#endif
817};
818
819static_assert (std::ranges::random_access_range<ArrangerObjectSpan>);
820
821}
static std::unique_ptr< Stretcher > create_rubberband(units::sample_rate_t samplerate, unsigned channels, double time_ratio, double pitch_ratio, bool realtime)
Create a new Stretcher using the rubberband backend.
Adds length functionality to arranger objects.
Track span that offers helper methods on a range of tracks.
bool can_be_pasted() const
Returns if the selections can be pasted at the current place/playhead.
auto merge() const -> ArrangerObjectUuidReference
Checks whether an object matches the given parameters.
std::optional< VariantType > get_bounded_object_at_position(units::sample_t pos_samples, bool include_region_end=false) const
Returns the region at the given position, or NULL.
static auto split_bounded_object(const BoundedObjectT &self, const auto &factory, int64_t global_pos) -> std::pair< ArrangerObjectUuidReference, ArrangerObjectUuidReference >
Splits the given object at the given position and returns a pair of newly-created objects (with uniqu...
bool contains_unclonable_object() const
Returns if the selections contain an unclonable object (such as the start marker).
bool contains_undeletable_object() const
Returns if the selections contain an undeletable object (such as the start marker).
bool contains_unrenamable_object() const
Whether the selections contain an unrenameable object (such as the start marker).
auto get_first_object_and_pos() const -> std::pair< VariantType, double >
Gets first object of the given type (if any, otherwise matches all types) and its start position.
auto get_last_object_and_pos(bool ends_last) const -> std::pair< VariantType, double >
Gets last object of the given type (if any, otherwise matches all types) and its end (if applicable,...
std::vector< VariantType > sort_by_indices(bool desc)
Sorts the selections by their indices (eg, for regions, their track indices, then the lane indices,...
A MIDI note inside a Region shown in the piano roll.
Definition midi_note.h:21
Lightweight UTF-8 string wrapper with safe conversions.
Definition utf8_string.h:37
A unified view over UUID-identified objects that supports:
void clip(std::span< float > buf, float minf, float maxf)
Clamp the buffer to min/max.