diff --git a/include/xstudio/playhead/enums.hpp b/include/xstudio/playhead/enums.hpp index 20ab7127b..17f618faf 100644 --- a/include/xstudio/playhead/enums.hpp +++ b/include/xstudio/playhead/enums.hpp @@ -7,7 +7,12 @@ typedef enum { CM_STRING = 0, CM_AB, CM_VERTICAL, CM_HORIZONTAL, CM_GRID, CM_OFF typedef enum { AM_STRING = 0, AM_ONE, AM_ALL, AM_TEN } AssemblyMode; -typedef enum { AAM_ALIGN_OFF = 0, AAM_ALIGN_FRAMES, AAM_ALIGN_TRIM } AutoAlignMode; +typedef enum { + AAM_ALIGN_OFF = 0, + AAM_ALIGN_FRAMES, + AAM_ALIGN_TRIM, + AAM_ALIGN_MANUAL +} AutoAlignMode; typedef enum { LM_PLAY_ONCE = 0, LM_LOOP, LM_PING_PONG } LoopMode; diff --git a/include/xstudio/playhead/playhead.hpp b/include/xstudio/playhead/playhead.hpp index 5af5c6503..e2815d85b 100644 --- a/include/xstudio/playhead/playhead.hpp +++ b/include/xstudio/playhead/playhead.hpp @@ -145,7 +145,8 @@ class PlayheadBase : public module::Module { auto_align_mode_names = { {AAM_ALIGN_OFF, "Off", "Off", true}, {AAM_ALIGN_FRAMES, "On", "On", true}, - {AAM_ALIGN_TRIM, "On (Trim)", "Trim", true}}; + {AAM_ALIGN_TRIM, "On (Trim)", "Trim", true}, + {AAM_ALIGN_MANUAL, "Manual", "Man.", true}}; utility::TimeSourceMode play_rate_mode_{utility::TimeSourceMode::DYNAMIC}; utility::FrameRate playhead_rate_; diff --git a/include/xstudio/playhead/playhead_actor.hpp b/include/xstudio/playhead/playhead_actor.hpp index f2146b289..b3b83b5e3 100644 --- a/include/xstudio/playhead/playhead_actor.hpp +++ b/include/xstudio/playhead/playhead_actor.hpp @@ -77,6 +77,7 @@ class PlayheadActor : public caf::event_based_actor, public PlayheadBase { void match_video_track_durations(); void align_audio_playhead(); void align_clip_frame_numbers(); + bool set_manual_source_offset(const utility::Uuid &media_uuid, const int64_t offset); void move_playhead_to_last_viewed_frame_of_current_source(); void move_playhead_to_last_viewed_frame_of_given_source(const utility::Uuid &source_uuid); void current_media_changed(caf::actor media_actor, const bool force = false); @@ -132,6 +133,10 @@ class PlayheadActor : public caf::event_based_actor, public PlayheadBase { utility::UuidActor hero_sub_playhead_; utility::UuidActorVector sub_playheads_; + // per-source compare offsets (media uuid -> frame offset), applied by + // align_clip_frame_numbers() when auto align mode is 'Manual' + std::map manual_source_offsets_; + utility::UuidActor video_string_out_actor_; utility::UuidActor timeline_actor_; utility::UuidActorVector source_actors_; diff --git a/python/src/xstudio/api/session/playhead/playhead.py b/python/src/xstudio/api/session/playhead/playhead.py index e2bd615d6..82f974137 100644 --- a/python/src/xstudio/api/session/playhead/playhead.py +++ b/python/src/xstudio/api/session/playhead/playhead.py @@ -1,6 +1,7 @@ # SPDX-License-Identifier: Apache-2.0 from xstudio.core import play_atom, loop_atom, compare_mode_atom, play_forward_atom from xstudio.core import logical_frame_atom, play_rate_mode_atom, source_atom, media_atom +from xstudio.core import source_offset_frames_atom from xstudio.core import simple_loop_start_atom, simple_loop_end_atom, use_loop_range_atom from xstudio.core import viewport_playhead_atom, media_logical_frame_atom, playhead_rate_atom from xstudio.core import JsonStore, change_attribute_value_atom, jump_atom @@ -217,3 +218,51 @@ def compare_mode(self, compare_mode): Args: compare_mode(str): The compare mode.""" self.attrs_by_name_["Compare"].set_value(compare_mode) + + @property + def auto_align_mode(self): + """Get the auto align mode. e.g. "Off", "On", "On (Trim)", "Manual" + + Returns: + auto_align_mode(str): The auto align mode. + """ + return self.attrs_by_name_["Auto Align"].value() + + @auto_align_mode.setter + def auto_align_mode(self, align_mode): + """Set the auto align mode. In "Manual" mode per-source compare + offsets set via set_source_offset_frames are preserved across + selection and compare mode changes. + + Args: + align_mode(str): "Off", "On", "On (Trim)" or "Manual".""" + self.attrs_by_name_["Auto Align"].set_value(align_mode) + + @property + def source_alignment_frames(self): + """Get the frame offsets currently applied to each compared source + (in selection order). + + Returns: + source_alignment_frames(list(int)): Per-source frame offsets. + """ + return self.attrs_by_name_["Source Alignment Frames"].value() + + def set_source_offset_frames(self, source, offset): + """Set the compare frame offset for one of the compared sources. + A positive offset plays the source earlier. The offset only sticks + while auto_align_mode is "Manual" - other align modes recompute + offsets on selection changes. + + Args: + source(Media|Uuid|int): The media (or its uuid, or its index in + the current selection) to offset. + offset(int): Frame offset. Positive plays the source earlier. + + Returns: + applied(bool): True if the source is in the current selection. + """ + if isinstance(source, Media): + source = source.uuid + return self.connection.request_receive( + self.remote, source_offset_frames_atom(), source, offset)[0] diff --git a/src/playhead/src/playhead_actor.cpp b/src/playhead/src/playhead_actor.cpp index 75eaf4962..f0c2e1072 100644 --- a/src/playhead/src/playhead_actor.cpp +++ b/src/playhead/src/playhead_actor.cpp @@ -471,6 +471,25 @@ void PlayheadActor::init() { } }, + [=](media::source_offset_frames_atom, + const utility::Uuid &media_uuid, + const int offset) -> result { + // set the compare offset for the source playing the given media. + // The offset is remembered against the media uuid so it survives + // selection and compare mode changes while auto align is 'Manual'. + return set_manual_source_offset(media_uuid, offset); + }, + + [=](media::source_offset_frames_atom, + const int sub_playhead_index, + const int offset) -> result { + // as above, addressing the source by its (selection) index + if (sub_playhead_index < 0 || + sub_playhead_index >= static_cast(source_actors_.size())) + return make_error(xstudio_error::error, "Source index out of range"); + return set_manual_source_offset(source_actors_[sub_playhead_index].uuid(), offset); + }, + [=](media_events_group_atom) -> caf::actor { return playhead_media_events_group_; }, [=](media_atom atom) { return mail(atom).delegate(hero_sub_playhead_.actor()); }, @@ -1505,14 +1524,22 @@ void PlayheadActor::init() { align_audio_playhead(); auto offset_frames = source_alignment_values_->value(); - size_t idx = 0; + const bool manual_align = + auto_align_mode() == AAM_ALIGN_MANUAL; + size_t idx = 0; for (auto &sub_playhead : sub_playheads_) { - if (idx < offset_frames.size()) + if (idx < offset_frames.size()) { + if (manual_align && idx < source_actors_.size()) + manual_source_offsets_ + [source_actors_[idx].uuid()] = + offset_frames[idx]; anon_mail( media::source_offset_frames_atom_v, - (int64_t)offset_frames[idx++], + (int64_t)offset_frames[idx], false) .send(sub_playhead.actor()); + } + idx++; } notify_loop_end_changed(); notify_loop_start_changed(); @@ -2597,8 +2624,11 @@ void PlayheadActor::align_clip_frame_numbers() { // in timeline_mode we always comparing video tracks from the same timeline. // We therefore do not need to align or trim - we just extend the duration // of video tracks to match the longest. - const bool align = timeline_mode() ? false : auto_align_mode() != AAM_ALIGN_OFF; - const bool trim = timeline_mode() ? false : auto_align_mode() == AAM_ALIGN_TRIM; + const AutoAlignMode align_mode = + timeline_mode() ? AAM_ALIGN_OFF : auto_align_mode(); + const bool align = align_mode == AAM_ALIGN_FRAMES || align_mode == AAM_ALIGN_TRIM; + const bool trim = align_mode == AAM_ALIGN_TRIM; + const bool manual = align_mode == AAM_ALIGN_MANUAL; // Use timecode to align the sources - if we are trimming, we use trim to the latest // start frame and the earliest end frame across all sources. If not we do the reverse, @@ -2656,6 +2686,28 @@ void PlayheadActor::align_clip_frame_numbers() { true // reset the any duration override that might have been applied ); } + } else if (manual) { + + // apply user-set per-source offsets, keyed against the media each + // sub-playhead is playing so offsets survive selection changes + for (size_t i = 0; i < sub_playheads_.size(); ++i) { + + int64_t offset = 0; + if (i < source_actors_.size()) { + auto p = manual_source_offsets_.find(source_actors_[i].uuid()); + if (p != manual_source_offsets_.end()) + offset = p->second; + } + + request_receive_wait( + *sys, + sub_playheads_[i].actor(), + timeout, + media::source_offset_frames_atom_v, + offset, + true // reset the duration + ); + } } else { for (const auto &sub_playhead : sub_playheads_) { @@ -2693,6 +2745,16 @@ void PlayheadActor::align_clip_frame_numbers() { *sys, hero_sub_playhead_.actor(), timeout, media::source_offset_frames_atom_v), false); + // reflect the offsets that were actually applied in the + // "Source Alignment Frames" attribute + std::vector applied_offsets; + applied_offsets.reserve(sub_playheads_.size()); + for (const auto &sub_playhead : sub_playheads_) { + applied_offsets.push_back(static_cast(request_receive_wait( + *sys, sub_playhead.actor(), timeout, media::source_offset_frames_atom_v))); + } + source_alignment_values_->set_value(applied_offsets, false); + // the cached frames display might need updating rebuild_cached_frames_status(); @@ -2710,6 +2772,34 @@ void PlayheadActor::align_clip_frame_numbers() { } } +bool PlayheadActor::set_manual_source_offset( + const utility::Uuid &media_uuid, const int64_t offset) { + + // the offset is remembered even if the media is not in the current + // selection (or auto align is not 'Manual' yet) so it can take effect + // when it is + manual_source_offsets_[media_uuid] = offset; + + bool in_selection = false; + for (const auto &source : source_actors_) { + if (source.uuid() == media_uuid) { + in_selection = true; + break; + } + } + + if (!timeline_mode() && auto_align_mode() == AAM_ALIGN_MANUAL && in_selection) { + align_clip_frame_numbers(); + align_audio_playhead(); + anon_mail(duration_flicks_atom_v).send(this); + // force broadcast of image buffers so the offset change shows + // in the viewport + anon_mail(jump_atom_v).send(this); + } + + return in_selection; +} + void PlayheadActor::move_playhead_to_last_viewed_frame_of_current_source() { try { @@ -2909,7 +2999,33 @@ void PlayheadActor::attribute_changed(const utility::Uuid &attr_uuid, const int } else if (attr_uuid == loop_range_enabled_->uuid()) { notify_loop_start_changed(); notify_loop_end_changed(); + } else if (attr_uuid == source_alignment_values_->uuid()) { + + if (!timeline_mode() && auto_align_mode() == AAM_ALIGN_MANUAL) { + // per-source offsets set through the attribute - remember them + // against the media of each sub-playhead and re-apply + const auto vals = source_alignment_values_->value(); + for (size_t i = 0; i < vals.size() && i < source_actors_.size(); ++i) { + manual_source_offsets_[source_actors_[i].uuid()] = vals[i]; + } + align_clip_frame_numbers(); + align_audio_playhead(); + anon_mail(duration_flicks_atom_v).send(this); + anon_mail(jump_atom_v).send(this); + } + } else if (attr_uuid == source_offset_frames_->uuid()) { + if (!timeline_mode() && auto_align_mode() == AAM_ALIGN_MANUAL) { + // remember the offset against the hero media so it persists + // across selection changes + for (size_t i = 0; i < sub_playheads_.size() && i < source_actors_.size(); ++i) { + if (sub_playheads_[i] == hero_sub_playhead_) { + manual_source_offsets_[source_actors_[i].uuid()] = + source_offset_frames_->value(); + break; + } + } + } mail(media::source_offset_frames_atom_v, source_offset_frames_->value(), false) .request(hero_sub_playhead_.actor(), infinite) .then( diff --git a/src/python_module/src/py_playhead.cpp b/src/python_module/src/py_playhead.cpp index 509aaa18c..f2ca68fce 100644 --- a/src/python_module/src/py_playhead.cpp +++ b/src/python_module/src/py_playhead.cpp @@ -32,6 +32,7 @@ void py_playhead(py::module_ &m) { .value("AAM_ALIGN_OFF", playhead::AutoAlignMode::AAM_ALIGN_OFF) .value("AAM_ALIGN_FRAMES", playhead::AutoAlignMode::AAM_ALIGN_FRAMES) .value("AAM_ALIGN_TRIM", playhead::AutoAlignMode::AAM_ALIGN_TRIM) + .value("AAM_ALIGN_MANUAL", playhead::AutoAlignMode::AAM_ALIGN_MANUAL) .export_values(); py::enum_(m, "LoopMode")