// Copyright (c) 2010, Niels Martin Hansen // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are met: // // * Redistributions of source code must retain the above copyright notice, // this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright notice, // this list of conditions and the following disclaimer in the documentation // and/or other materials provided with the distribution. // * Neither the name of the Aegisub Group nor the names of its contributors // may be used to endorse or promote products derived from this software // without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE // POSSIBILITY OF SUCH DAMAGE. // // Aegisub Project http://www.aegisub.org/ /// @file audio_timing_dialogue.cpp /// @brief Default timing mode for dialogue subtitles /// @ingroup audio_ui #include #include #include "ass_dialogue.h" #include "ass_file.h" #include "ass_time.h" #include "audio_renderer.h" #include "audio_timing.h" #include "command/command.h" #include "include/aegisub/context.h" #include "options.h" #include "pen.h" #include "selection_controller.h" #include "utils.h" class TimeableLine; /// @class DialogueTimingMarker /// @brief AudioMarker implementation for AudioTimingControllerDialogue /// /// Audio marker intended to live in pairs of two, taking styles depending /// on which marker in the pair is to the left and which is to the right. class DialogueTimingMarker final : public AudioMarker { /// Current ms position of this marker int position; /// Draw style for the marker const Pen *style; /// Feet style for the marker FeetStyle feet; /// Rendering style of the owning line, needed for sorting AudioRenderingStyle type; /// The line which owns this marker TimeableLine *line; public: int GetPosition() const override { return position; } wxPen GetStyle() const override { return *style; } FeetStyle GetFeet() const override { return feet; } bool CanSnap() const override { return true; } /// Move the marker to a new position /// @param new_position The position to move the marker to, in milliseconds /// /// This notifies the owning line of the change, so that it can ensure that /// this marker has the appropriate rendering style. void SetPosition(int new_position); /// Constructor /// @param position Initial position of this marker /// @param style Rendering style of this marker /// @param feet Foot style of this marker /// @param type Type of this marker, used only for sorting /// @param line Line which this is a marker for DialogueTimingMarker(int position, const Pen *style, FeetStyle feet, AudioRenderingStyle type, TimeableLine *line) : position(position) , style(style) , feet(feet) , type(type) , line(line) { } DialogueTimingMarker(DialogueTimingMarker const& other, TimeableLine *line) : position(other.position) , style(other.style) , feet(other.feet) , type(other.type) , line(line) { } /// Get the line which this is a marker for TimeableLine *GetLine() const { return line; } /// Implicit decay to the position of the marker operator int() const { return position; } /// Comparison operator /// /// Compares first on position, then on audio rendering style so that the /// markers for the active line end up after those for the inactive lines. bool operator<(DialogueTimingMarker const& other) const { if (position < other.position) return true; if (position > other.position) return false; return type < other.type; } /// Swap the rendering style of this marker with that of the passed marker void SwapStyles(DialogueTimingMarker &other) { std::swap(style, other.style); std::swap(feet, other.feet); } }; /// A comparison predicate for pointers to dialogue markers and millisecond positions struct marker_ptr_cmp { bool operator()(const DialogueTimingMarker *lft, const DialogueTimingMarker *rgt) const { return *lft < *rgt; } bool operator()(const DialogueTimingMarker *lft, int rgt) const { return *lft < rgt; } bool operator()(int lft, const DialogueTimingMarker *rgt) const { return lft < *rgt; } }; /// @class TimeableLine /// @brief A single dialogue line which can be timed via AudioTimingControllerDialogue /// /// This class provides markers and styling ranges for a single dialogue line, /// both active and inactive. In addition, it can apply changes made via those /// markers to the tracked dialogue line. class TimeableLine { /// The current tracked dialogue line AssDialogue *line; /// The rendering style of this line AudioRenderingStyle style; /// One of the markers. Initially the left marker, but the user may change this. DialogueTimingMarker marker1; /// One of the markers. Initially the right marker, but the user may change this. DialogueTimingMarker marker2; /// Pointer to whichever marker happens to be on the left DialogueTimingMarker *left_marker; /// Pointer to whichever marker happens to be on the right DialogueTimingMarker *right_marker; public: /// Constructor /// @param style Rendering style to use for this line's time range /// @param style_left The rendering style for the start marker /// @param style_right The rendering style for the end marker TimeableLine(AudioRenderingStyle style, const Pen *style_left, const Pen *style_right) : line(nullptr) , style(style) , marker1(0, style_left, AudioMarker::Feet_Right, style, this) , marker2(0, style_right, AudioMarker::Feet_Left, style, this) , left_marker(&marker1) , right_marker(&marker2) { } /// Explicit copy constructor needed due to that the markers have a pointer to this TimeableLine(TimeableLine const& other) : line(other.line) , style(other.style) , marker1(*other.left_marker, this) , marker2(*other.right_marker, this) , left_marker(&marker1) , right_marker(&marker2) { } /// Get the tracked dialogue line AssDialogue *GetLine() const { return line; } /// Get the time range for this line operator TimeRange() const { return TimeRange(*left_marker, *right_marker); } /// Add this line's style to the style ranges void GetStyleRange(AudioRenderingStyleRanges *ranges) const { ranges->AddRange(*left_marker, *right_marker, style); } /// Get this line's markers /// @param c Vector to add the markers to void GetMarkers(std::vector *c) const { c->push_back(left_marker); c->push_back(right_marker); } /// Get the leftmost of the markers DialogueTimingMarker *GetLeftMarker() { return left_marker; } /// Get the rightmost of the markers DialogueTimingMarker *GetRightMarker() { return right_marker; } /// Does this line have a marker in the given range? bool ContainsMarker(TimeRange const& range) const { return range.contains(marker1) || range.contains(marker2); } /// Check if the markers have the correct styles, and correct them if needed void CheckMarkers() { if (*right_marker < *left_marker) { marker1.SwapStyles(marker2); std::swap(left_marker, right_marker); } } /// Apply any changes made here to the tracked dialogue line void Apply() { if (line) { line->Start = left_marker->GetPosition(); line->End = right_marker->GetPosition(); } } /// Set the dialogue line which this is tracking and reset the markers to /// the line's time range /// @return Were the markers actually set to the line's time? bool SetLine(AssDialogue *new_line) { if (!line || new_line->End > 0) { line = new_line; marker1.SetPosition(new_line->Start); marker2.SetPosition(new_line->End); return true; } else { line = new_line; return false; } } }; void DialogueTimingMarker::SetPosition(int new_position) { position = new_position; line->CheckMarkers(); } /// @class AudioTimingControllerDialogue /// @brief Default timing mode for dialogue subtitles /// /// Displays a start and end marker for an active subtitle line, and possibly /// some of the inactive lines. The markers for the active line can be dragged, /// updating the audio selection and the start/end time of that line. In /// addition, any markers for inactive lines that start/end at the same time /// as the active line starts/ends can optionally be dragged along with the /// active line's markers, updating those lines as well. class AudioTimingControllerDialogue final : public AudioTimingController { /// The rendering style for the active line's start marker Pen style_left; /// The rendering style for the active line's end marker Pen style_right; /// The rendering style for the start and end markers of inactive lines Pen style_inactive; /// The currently active line TimeableLine active_line; /// Inactive lines which are currently modifiable std::list inactive_lines; /// Selected lines which are currently modifiable std::list selected_lines; /// All audio markers for active and inactive lines, sorted by position std::vector markers; /// Marker provider for video keyframes AudioMarkerProviderKeyframes keyframes_provider; /// Marker provider for video playback position VideoPositionMarkerProvider video_position_provider; /// Marker provider for seconds lines SecondsMarkerProvider seconds_provider; /// The set of lines which have been modified and need to have their /// changes applied on commit std::set modified_lines; /// Commit id for coalescing purposes when in auto commit mode int commit_id; /// The owning project context agi::Context *context; /// Autocommit option const agi::OptionValue *auto_commit; const agi::OptionValue *inactive_line_mode; const agi::OptionValue *inactive_line_comments; const agi::OptionValue *drag_timing; agi::signal::Connection commit_connection; agi::signal::Connection audio_open_connection; agi::signal::Connection inactive_line_mode_connection; agi::signal::Connection inactive_line_comment_connection; agi::signal::Connection active_line_connection; agi::signal::Connection selection_connection; /// Update the audio controller's selection void UpdateSelection(); /// Regenerate the list of timeable inactive lines void RegenerateInactiveLines(); /// Regenerate the list of timeable selected lines void RegenerateSelectedLines(); /// Add a line to the list of timeable inactive lines void AddInactiveLine(SubtitleSelection const& sel, AssDialogue *diag); /// Regenerate the list of active and inactive line markers void RegenerateMarkers(); /// Get the start markers for the active line and all selected lines std::vector GetLeftMarkers(); /// Get the end markers for the active line and all selected lines std::vector GetRightMarkers(); /// @brief Set the position of markers and announce the change to the world /// @param upd_markers Markers to move /// @param ms New position of the markers void SetMarkers(std::vector const& upd_markers, int ms); /// Snap a position to a nearby marker, if any /// @param position Position to snap /// @param snap_range Maximum distance to snap in milliseconds /// @param exclude Markers which should be excluded from the potential snaps int SnapPosition(int position, int snap_range, std::vector const& exclude) const; /// Commit all pending changes to the file /// @param user_triggered Is this a user-initiated commit or an autocommit void DoCommit(bool user_triggered); void OnActiveLineChanged(); void OnSelectedSetChanged(); // AssFile events void OnFileChanged(int type); public: // AudioMarkerProvider interface void GetMarkers(const TimeRange &range, AudioMarkerVector &out_markers) const override; // AudioTimingController interface wxString GetWarningMessage() const override; TimeRange GetIdealVisibleTimeRange() const override; TimeRange GetPrimaryPlaybackRange() const override; TimeRange GetActiveLineRange() const override; void GetRenderingStyles(AudioRenderingStyleRanges &ranges) const override; void GetLabels(TimeRange const& range, std::vector &out) const override { } void Next(NextMode mode) override; void Prev() override; void Commit() override; void Revert() override; void AddLeadIn() override; void AddLeadOut() override; void ModifyLength(int delta, bool shift_following) override; void ModifyStart(int delta) override; bool IsNearbyMarker(int ms, int sensitivity) const override; std::vector OnLeftClick(int ms, bool ctrl_down, int sensitivity, int snap_range) override; std::vector OnRightClick(int ms, bool, int sensitivity, int snap_range) override; void OnMarkerDrag(std::vector const& markers, int new_position, int snap_range) override; /// Constructor /// @param c Project context AudioTimingControllerDialogue(agi::Context *c); }; AudioTimingController *CreateDialogueTimingController(agi::Context *c) { return new AudioTimingControllerDialogue(c); } AudioTimingControllerDialogue::AudioTimingControllerDialogue(agi::Context *c) : style_left("Colour/Audio Display/Line boundary Start", "Audio/Line Boundaries Thickness") , style_right("Colour/Audio Display/Line boundary End", "Audio/Line Boundaries Thickness") , style_inactive("Colour/Audio Display/Line Boundary Inactive Line", "Audio/Line Boundaries Thickness") , active_line(AudioStyle_Primary, &style_left, &style_right) , keyframes_provider(c, "Audio/Display/Draw/Keyframes in Dialogue Mode") , video_position_provider(c) , commit_id(-1) , context(c) , auto_commit(OPT_GET("Audio/Auto/Commit")) , inactive_line_mode(OPT_GET("Audio/Inactive Lines Display Mode")) , inactive_line_comments(OPT_GET("Audio/Display/Draw/Inactive Comments")) , drag_timing(OPT_GET("Audio/Drag Timing")) , commit_connection(c->ass->AddCommitListener(&AudioTimingControllerDialogue::OnFileChanged, this)) , inactive_line_mode_connection(OPT_SUB("Audio/Inactive Lines Display Mode", &AudioTimingControllerDialogue::RegenerateInactiveLines, this)) , inactive_line_comment_connection(OPT_SUB("Audio/Display/Draw/Inactive Comments", &AudioTimingControllerDialogue::RegenerateInactiveLines, this)) , active_line_connection(c->selectionController->AddActiveLineListener(&AudioTimingControllerDialogue::OnActiveLineChanged, this)) , selection_connection(c->selectionController->AddSelectionListener(&AudioTimingControllerDialogue::OnSelectedSetChanged, this)) { keyframes_provider.AddMarkerMovedListener(std::bind(std::ref(AnnounceMarkerMoved))); video_position_provider.AddMarkerMovedListener(std::bind(std::ref(AnnounceMarkerMoved))); seconds_provider.AddMarkerMovedListener(std::bind(std::ref(AnnounceMarkerMoved))); Revert(); } void AudioTimingControllerDialogue::GetMarkers(const TimeRange &range, AudioMarkerVector &out_markers) const { // The order matters here; later markers are painted on top of earlier // markers, so the markers that we want to end up on top need to appear last seconds_provider.GetMarkers(range, out_markers); // Copy inactive line markers in the range copy( lower_bound(markers.begin(), markers.end(), range.begin(), marker_ptr_cmp()), upper_bound(markers.begin(), markers.end(), range.end(), marker_ptr_cmp()), back_inserter(out_markers)); keyframes_provider.GetMarkers(range, out_markers); video_position_provider.GetMarkers(range, out_markers); } void AudioTimingControllerDialogue::OnActiveLineChanged() { Revert(); } void AudioTimingControllerDialogue::OnSelectedSetChanged() { RegenerateSelectedLines(); RegenerateInactiveLines(); } void AudioTimingControllerDialogue::OnFileChanged(int type) { if (type & AssFile::COMMIT_DIAG_TIME) Revert(); else if (type & AssFile::COMMIT_DIAG_ADDREM) RegenerateInactiveLines(); } wxString AudioTimingControllerDialogue::GetWarningMessage() const { // We have no warning messages currently, maybe add the old "Modified" message back later? return wxString(); } TimeRange AudioTimingControllerDialogue::GetIdealVisibleTimeRange() const { return GetPrimaryPlaybackRange(); } TimeRange AudioTimingControllerDialogue::GetPrimaryPlaybackRange() const { return active_line; } TimeRange AudioTimingControllerDialogue::GetActiveLineRange() const { return active_line; } void AudioTimingControllerDialogue::GetRenderingStyles(AudioRenderingStyleRanges &ranges) const { active_line.GetStyleRange(&ranges); for_each(selected_lines.begin(), selected_lines.end(), std::bind(&TimeableLine::GetStyleRange, std::placeholders::_1, &ranges)); for_each(inactive_lines.begin(), inactive_lines.end(), std::bind(&TimeableLine::GetStyleRange, std::placeholders::_1, &ranges)); } void AudioTimingControllerDialogue::Next(NextMode mode) { if (mode == TIMING_UNIT) { context->selectionController->NextLine(); return; } int new_end_ms = *active_line.GetRightMarker(); cmd::call("grid/line/next/create", context); if (mode == LINE_RESET_DEFAULT || active_line.GetLine()->End == 0) { const int default_duration = OPT_GET("Timing/Default Duration")->GetInt(); // Setting right first here so that they don't get switched and the // same marker gets set twice active_line.GetRightMarker()->SetPosition(new_end_ms + default_duration); active_line.GetLeftMarker()->SetPosition(new_end_ms); sort(markers.begin(), markers.end(), marker_ptr_cmp()); modified_lines.insert(&active_line); UpdateSelection(); } } void AudioTimingControllerDialogue::Prev() { context->selectionController->PrevLine(); } void AudioTimingControllerDialogue::Commit() { DoCommit(true); } void AudioTimingControllerDialogue::DoCommit(bool user_triggered) { // Store back new times if (modified_lines.size()) { for_each(modified_lines.begin(), modified_lines.end(), std::mem_fn(&TimeableLine::Apply)); commit_connection.Block(); if (user_triggered) { context->ass->Commit(_("timing"), AssFile::COMMIT_DIAG_TIME); commit_id = -1; // never coalesce with a manually triggered commit } else { AssDialogue *amend = modified_lines.size() == 1 ? (*modified_lines.begin())->GetLine() : nullptr; commit_id = context->ass->Commit(_("timing"), AssFile::COMMIT_DIAG_TIME, commit_id, amend); } commit_connection.Unblock(); modified_lines.clear(); } } void AudioTimingControllerDialogue::Revert() { commit_id = -1; if (AssDialogue *line = context->selectionController->GetActiveLine()) { modified_lines.clear(); if (active_line.SetLine(line)) { AnnounceUpdatedPrimaryRange(); if (inactive_line_mode->GetInt() == 0) AnnounceUpdatedStyleRanges(); } else { modified_lines.insert(&active_line); } } RegenerateInactiveLines(); RegenerateSelectedLines(); } void AudioTimingControllerDialogue::AddLeadIn() { DialogueTimingMarker *m = active_line.GetLeftMarker(); SetMarkers({ m }, *m - OPT_GET("Audio/Lead/IN")->GetInt()); } void AudioTimingControllerDialogue::AddLeadOut() { DialogueTimingMarker *m = active_line.GetRightMarker(); SetMarkers({ m }, *m + OPT_GET("Audio/Lead/OUT")->GetInt()); } void AudioTimingControllerDialogue::ModifyLength(int delta, bool) { DialogueTimingMarker *m = active_line.GetRightMarker(); SetMarkers({ m }, std::max(*m + delta * 10, *active_line.GetLeftMarker())); } void AudioTimingControllerDialogue::ModifyStart(int delta) { DialogueTimingMarker *m = active_line.GetLeftMarker(); SetMarkers({ m }, std::min(*m + delta * 10, *active_line.GetRightMarker())); } bool AudioTimingControllerDialogue::IsNearbyMarker(int ms, int sensitivity) const { assert(sensitivity >= 0); return active_line.ContainsMarker(TimeRange(ms-sensitivity, ms+sensitivity)); } std::vector AudioTimingControllerDialogue::OnLeftClick(int ms, bool ctrl_down, int sensitivity, int snap_range) { assert(sensitivity >= 0); assert(snap_range >= 0); std::vector ret; DialogueTimingMarker *left = active_line.GetLeftMarker(); DialogueTimingMarker *right = active_line.GetRightMarker(); int dist_l = tabs(*left - ms); int dist_r = tabs(*right - ms); if (dist_l > sensitivity && dist_r > sensitivity) { // Clicked far from either marker: // Insta-set the left marker to the clicked position and return the // right as the dragged one, such that if the user does start dragging, // he will create a new selection from scratch std::vector jump = GetLeftMarkers(); ret = drag_timing->GetBool() ? GetRightMarkers() : jump; // Get ret before setting as setting may swap left/right SetMarkers(jump, SnapPosition(ms, snap_range, jump)); return ret; } DialogueTimingMarker *clicked = dist_l <= dist_r ? left : right; if (ctrl_down) { // The use of GetPosition here is important, as otherwise it'll start // after lines ending at the same time as the active line begins auto it = lower_bound(markers.begin(), markers.end(), clicked->GetPosition(), marker_ptr_cmp()); for(; it != markers.end() && !(*clicked < **it); ++it) ret.push_back(*it); } else ret.push_back(clicked); // Left-click within drag range should still move the left marker to the // clicked position, but not the right marker if (clicked == left) SetMarkers(ret, SnapPosition(ms, snap_range, ret)); return ret; } std::vector AudioTimingControllerDialogue::OnRightClick(int ms, bool, int sensitivity, int snap_range) { std::vector ret = GetRightMarkers(); SetMarkers(ret, SnapPosition(ms, snap_range, ret)); return ret; } void AudioTimingControllerDialogue::OnMarkerDrag(std::vector const& markers, int new_position, int snap_range) { SetMarkers(markers, SnapPosition(new_position, snap_range, markers)); } void AudioTimingControllerDialogue::UpdateSelection() { AnnounceUpdatedPrimaryRange(); AnnounceUpdatedStyleRanges(); } void AudioTimingControllerDialogue::SetMarkers(std::vector const& upd_markers, int ms) { // Since we're moving markers, the sorted list of markers will need to be // resorted. To avoid resorting the entire thing, find the subrange that // is effected. int min_ms = ms; int max_ms = ms; for (AudioMarker *upd_marker : upd_markers) { DialogueTimingMarker *marker = static_cast(upd_marker); min_ms = std::min(*marker, min_ms); max_ms = std::max(*marker, max_ms); } auto begin = lower_bound(markers.begin(), markers.end(), min_ms, marker_ptr_cmp()); auto end = upper_bound(begin, markers.end(), max_ms, marker_ptr_cmp()); // Update the markers for (AudioMarker *upd_marker : upd_markers) { DialogueTimingMarker *marker = static_cast(upd_marker); marker->SetPosition(ms); modified_lines.insert(marker->GetLine()); } // Resort the range sort(begin, end, marker_ptr_cmp()); if (auto_commit->GetBool()) DoCommit(false); UpdateSelection(); AnnounceMarkerMoved(); } static bool noncomment_dialogue(AssDialogue const& diag) { return !diag.Comment; } static bool dialogue(AssDialogue const&) { return true; } void AudioTimingControllerDialogue::RegenerateInactiveLines() { auto predicate = inactive_line_comments->GetBool() ? dialogue : noncomment_dialogue; bool was_empty = inactive_lines.empty(); inactive_lines.clear(); SubtitleSelection const& sel = context->selectionController->GetSelectedSet(); switch (int mode = inactive_line_mode->GetInt()) { case 1: // Previous line only case 2: // Previous and next lines if (AssDialogue *line = context->selectionController->GetActiveLine()) { auto current_line = context->ass->Events.iterator_to(*line); if (current_line == context->ass->Events.end()) break; auto prev = current_line; while (--prev != context->ass->Events.begin() && !predicate(*prev)) ; if (prev != context->ass->Events.begin()) AddInactiveLine(sel, &*prev); if (mode == 2) { auto next = std::find_if(++current_line, context->ass->Events.end(), predicate); if (next != context->ass->Events.end()) AddInactiveLine(sel, &*next); } } break; case 3: // All inactive lines { AssDialogue *active_line = context->selectionController->GetActiveLine(); for (auto& line : context->ass->Events) { if (&line != active_line && predicate(line)) AddInactiveLine(sel, &line); } break; } default: if (was_empty) { RegenerateMarkers(); return; } } AnnounceUpdatedStyleRanges(); RegenerateMarkers(); } void AudioTimingControllerDialogue::AddInactiveLine(SubtitleSelection const& sel, AssDialogue *diag) { if (sel.count(diag)) return; inactive_lines.emplace_back(AudioStyle_Inactive, &style_inactive, &style_inactive); inactive_lines.back().SetLine(diag); } void AudioTimingControllerDialogue::RegenerateSelectedLines() { bool was_empty = selected_lines.empty(); selected_lines.clear(); AssDialogue *active = context->selectionController->GetActiveLine(); SubtitleSelection const& sel = context->selectionController->GetSelectedSet(); for (auto line : sel) { if (line == active) continue; selected_lines.emplace_back(AudioStyle_Selected, &style_inactive, &style_inactive); selected_lines.back().SetLine(line); } if (!selected_lines.empty() || !was_empty) { AnnounceUpdatedStyleRanges(); RegenerateMarkers(); } } void AudioTimingControllerDialogue::RegenerateMarkers() { markers.clear(); active_line.GetMarkers(&markers); for_each(selected_lines.begin(), selected_lines.end(), std::bind(&TimeableLine::GetMarkers, std::placeholders::_1, &markers)); for_each(inactive_lines.begin(), inactive_lines.end(), std::bind(&TimeableLine::GetMarkers, std::placeholders::_1, &markers)); sort(markers.begin(), markers.end(), marker_ptr_cmp()); AnnounceMarkerMoved(); } std::vector AudioTimingControllerDialogue::GetLeftMarkers() { std::vector ret; ret.reserve(selected_lines.size() + 1); ret.push_back(active_line.GetLeftMarker()); transform(selected_lines.begin(), selected_lines.end(), back_inserter(ret), std::bind(&TimeableLine::GetLeftMarker, std::placeholders::_1)); return ret; } std::vector AudioTimingControllerDialogue::GetRightMarkers() { std::vector ret; ret.reserve(selected_lines.size() + 1); ret.push_back(active_line.GetRightMarker()); transform(selected_lines.begin(), selected_lines.end(), back_inserter(ret), std::bind(&TimeableLine::GetRightMarker, std::placeholders::_1)); return ret; } int AudioTimingControllerDialogue::SnapPosition(int position, int snap_range, std::vector const& exclude) const { if (position < 0) position = 0; if (snap_range <= 0) return position; TimeRange snap_time_range(position - snap_range, position + snap_range); const AudioMarker *snap_marker = nullptr; AudioMarkerVector potential_snaps; GetMarkers(snap_time_range, potential_snaps); for (auto marker : potential_snaps) { if (marker->CanSnap() && find(exclude.begin(), exclude.end(), marker) == exclude.end()) { if (!snap_marker) snap_marker = marker; else if (tabs(marker->GetPosition() - position) < tabs(snap_marker->GetPosition() - position)) snap_marker = marker; } } if (snap_marker) return snap_marker->GetPosition(); return position; }