Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion include/xstudio/playhead/enums.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
3 changes: 2 additions & 1 deletion include/xstudio/playhead/playhead.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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_;
Expand Down
5 changes: 5 additions & 0 deletions include/xstudio/playhead/playhead_actor.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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<utility::Uuid, int64_t> manual_source_offsets_;

utility::UuidActor video_string_out_actor_;
utility::UuidActor timeline_actor_;
utility::UuidActorVector source_actors_;
Expand Down
49 changes: 49 additions & 0 deletions python/src/xstudio/api/session/playhead/playhead.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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]
126 changes: 121 additions & 5 deletions src/playhead/src/playhead_actor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,25 @@ void PlayheadActor::init() {
}
},

[=](media::source_offset_frames_atom,
const utility::Uuid &media_uuid,
const int offset) -> result<bool> {
// 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<bool> {
// as above, addressing the source by its (selection) index
if (sub_playhead_index < 0 ||
sub_playhead_index >= static_cast<int>(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()); },
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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<bool>(
*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_) {
Expand Down Expand Up @@ -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<int> applied_offsets;
applied_offsets.reserve(sub_playheads_.size());
for (const auto &sub_playhead : sub_playheads_) {
applied_offsets.push_back(static_cast<int>(request_receive_wait<int64_t>(
*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();

Expand All @@ -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 {
Expand Down Expand Up @@ -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(
Expand Down
1 change: 1 addition & 0 deletions src/python_module/src/py_playhead.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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_<playhead::LoopMode>(m, "LoopMode")
Expand Down