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