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