Skip to content

Commit d77eb5b

Browse files
committed
Fix snapping behavior of current video position marker
Lots of text incoming, since timestamp wrangling is hell and I want to write down my reasoning before I inevitably forget it again in two weeks. Before commit 5d4973a, snapping an audio line's end time to the "current video position" indicator line would reliably make the line end on the frame before the current video frame (i.e. make the current video frame be the first frame where the selected line is *not* visible any more). Commit 5d4973a broke this, making the behavior of snapping audio times to the "current video position" inconsistent. The commit in question is definitely not fully correct, ultimately just because the entire concept of implicitly converting millisecond timestamps to centisecond timestamps is flawed in and of itself, and bound to always fail in sufficiently crazy edge cases. Fixing the timestamp conversion properly would entail either working with centisecond timestamps from the beginning, or somehow making the process of converting timestamps aware of the context the timestamps are from (e.g. "coming from some frame timestamp"). However, this specific issue was only *exposed* by this commit, and not solely caused by it. Its root cause was just that the "current video position" marker on the audio display would mark the *exact* time of the current video frame, rather than the ideal start/end time for a line to start/end at that video frame. This is why only snapping to the "current video position" broke while snapping to keyframes worked fine. It's quite possible that snapping to the "current video frame" marker was just never thought of as a use case. So, for now, TypesettingTools#421 can just be fixed by making a line snapped to the "current video position" marker snap to the middle of the frame rather than at the frame's exact start, and 5d4973a can be untangled at some later time. However, we would still like to *draw* the "current video position" marker at the exact start time of the current video frame, so that one can tell from the audio display which lines are visible at the current video frame and which aren't. Hence, we use the following middle-ground solution: - Keep drawing the "current video position" marker at the exact start time of video frames - Do not allow snapping to the exact time of the "current video position" marker. Instead, allow either snapping to the middle of the previous frame, or the middle of the current frame. - To make this behavior understandable for the user, draw a semi-transparent rectangle behind the "current video position" marker indicating the duration of the previous and current video frame. Fixes TypesettingTools#421.
1 parent 03818a3 commit d77eb5b

8 files changed

Lines changed: 103 additions & 16 deletions

src/audio_display.cpp

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
#include <algorithm>
5151

5252
#include <wx/dcbuffer.h>
53+
#include <wx/dcgraph.h>
5354
#include <wx/mousestate.h>
5455

5556
/// @class AudioDisplayInteractionObject
@@ -848,12 +849,13 @@ void AudioDisplay::OnPaint(wxPaintEvent&)
848849

849850
if (audio_bounds.Intersects(updrect))
850851
{
852+
wxGCDC gcdc(dc); // Markers may use alpha, which needs a GCDC
851853
TimeRange updtime(
852854
std::max(0, TimeFromRelativeX(updrect.x - foot_size)),
853855
std::max(0, TimeFromRelativeX(updrect.x + updrect.width + foot_size)));
854856

855857
PaintAudio(dc, updtime, updrect);
856-
PaintMarkers(dc, updtime);
858+
PaintMarkers(gcdc, updtime);
857859
PaintLabels(dc, updtime);
858860
}
859861
}
@@ -898,8 +900,17 @@ void AudioDisplay::PaintMarkers(wxDC &dc, TimeRange updtime)
898900
{
899901
int marker_x = RelativeXFromTime(marker->GetPosition());
900902

901-
dc.SetPen(marker->GetStyle());
902-
dc.DrawLine(marker_x, audio_top, marker_x, audio_top+audio_height);
903+
if (marker->GetWidth() == 0) {
904+
dc.SetPen(marker->GetStyle());
905+
dc.DrawLine(marker_x, audio_top, marker_x, audio_top+audio_height);
906+
} else {
907+
int w = RelativeXFromTime(marker->GetPosition() + marker->GetWidth() - 1) - marker_x + 1;
908+
909+
dc.SetBrush(wxBrush(marker->GetStyle().GetColour()));
910+
dc.SetPen(*wxTRANSPARENT_PEN);
911+
912+
dc.DrawRectangle(marker_x, audio_top, w, audio_height);
913+
}
903914

904915
if (marker->GetFeet() == AudioMarker::Feet_None) continue;
905916

src/audio_marker.cpp

Lines changed: 64 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,36 @@ void AudioMarkerProviderKeyframes::GetMarkers(TimeRange const& range, AudioMarke
8282
out.push_back(&*a);
8383
}
8484

85+
class VideoPositionSnapPoint final : public AudioMarker {
86+
int position = -1;
87+
88+
public:
89+
void SetPosition(int new_pos) { position = new_pos; }
90+
91+
int GetPosition() const override { return position; }
92+
FeetStyle GetFeet() const override { return Feet_None; }
93+
wxPen GetStyle() const override { return wxPen{}; }
94+
};
95+
96+
class VideoPositionRange final : public AudioMarker {
97+
Pen style;
98+
int position = -1;
99+
int width = 0;
100+
101+
public:
102+
VideoPositionRange(bool previous) : style{previous ? "Colour/Audio Display/Previous Frame Range" : "Colour/Audio Display/Current Frame Range"} { }
103+
104+
void SetPosition(int frame, int nextframe) {
105+
position = frame + 1;
106+
width = std::max(1, nextframe - frame);
107+
}
108+
109+
int GetPosition() const override { return position; }
110+
int GetWidth() const override { return width; }
111+
FeetStyle GetFeet() const override { return Feet_None; }
112+
wxPen GetStyle() const override { return style; }
113+
};
114+
85115
class VideoPositionMarker final : public AudioMarker {
86116
Pen style{"Colour/Audio Display/Play Cursor"};
87117
int position = -1;
@@ -92,7 +122,6 @@ class VideoPositionMarker final : public AudioMarker {
92122
int GetPosition() const override { return position; }
93123
FeetStyle GetFeet() const override { return Feet_None; }
94124
wxPen GetStyle() const override { return style; }
95-
operator int() const { return position; }
96125
};
97126

98127
VideoPositionMarkerProvider::VideoPositionMarkerProvider(agi::Context *c)
@@ -105,33 +134,61 @@ VideoPositionMarkerProvider::VideoPositionMarkerProvider(agi::Context *c)
105134

106135
VideoPositionMarkerProvider::~VideoPositionMarkerProvider() { }
107136

108-
void VideoPositionMarkerProvider::SetPosition(int frame_number) {
109-
marker->SetPosition(c->videoController->TimeAtFrame(frame_number));
137+
void VideoPositionMarkerProvider::SetPositions(int frame_number) {
138+
auto vc = c->videoController.get();
139+
140+
range1->SetPosition(vc->TimeAtFrame(frame_number - 1), vc->TimeAtFrame(frame_number));
141+
range2->SetPosition(vc->TimeAtFrame(frame_number), vc->TimeAtFrame(frame_number + 1));
142+
marker->SetPosition(vc->TimeAtFrame(frame_number));
143+
snap1->SetPosition(vc->TimeAtFrame(frame_number, agi::vfr::START));
144+
snap2->SetPosition(vc->TimeAtFrame(frame_number, agi::vfr::END));
110145
}
111146

112147
void VideoPositionMarkerProvider::Update(int frame_number) {
113-
SetPosition(frame_number);
148+
SetPositions(frame_number);
114149
AnnounceMarkerMoved();
115150
}
116151

117152
void VideoPositionMarkerProvider::OptChanged(agi::OptionValue const& opt) {
118153
if (opt.GetBool()) {
119154
video_seek_slot.Unblock();
155+
range1 = std::make_unique<VideoPositionRange>(true);
156+
range2 = std::make_unique<VideoPositionRange>(false);
120157
marker = std::make_unique<VideoPositionMarker>();
121-
SetPosition(c->videoController->GetFrameN());
158+
snap1 = std::make_unique<VideoPositionSnapPoint>();
159+
snap2 = std::make_unique<VideoPositionSnapPoint>();
160+
SetPositions(c->videoController->GetFrameN());
122161
}
123162
else {
124163
video_seek_slot.Block();
164+
range1.reset();
165+
range2.reset();
125166
marker.reset();
167+
snap1.reset();
168+
snap2.reset();
126169
}
127170
}
128171

129-
void VideoPositionMarkerProvider::GetMarkers(const TimeRange &range, AudioMarkerVector &out) const {
172+
void VideoPositionMarkerProvider::GetMarkers(const TimeRange &timerange, AudioMarkerVector &out) const {
130173
if (!c->project->VideoProvider())
131174
return;
132175

133-
if (marker && range.contains(*marker))
176+
for (auto range : {range1.get(), range2.get()})
177+
if (range && timerange.overlaps({range->GetPosition(), range->GetPosition() + range->GetWidth() - 1}))
178+
out.push_back(range);
179+
180+
if (marker && timerange.contains(marker->GetPosition()))
134181
out.push_back(marker.get());
182+
183+
}
184+
185+
void VideoPositionMarkerProvider::GetSnapMarkers(const TimeRange &timerange, AudioMarkerVector &out) const {
186+
if (!c->project->VideoProvider())
187+
return;
188+
189+
for (auto snap : {snap1.get(), snap2.get()})
190+
if (snap && timerange.contains(snap->GetPosition()))
191+
out.push_back(snap);
135192
}
136193

137194
SecondsMarkerProvider::SecondsMarkerProvider()

src/audio_marker.h

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ class AudioMarkerKeyframe;
2828
class Pen;
2929
class Project;
3030
class VideoPositionMarker;
31+
class VideoPositionRange;
32+
class VideoPositionSnapPoint;
3133
class wxPen;
3234

3335
namespace agi {
@@ -53,6 +55,10 @@ class AudioMarker {
5355
/// @return The marker's position in milliseconds
5456
virtual int GetPosition() const = 0;
5557

58+
/// @brief Get the marker's width
59+
/// @return The marker's width if the marker should be a rectangle, or 0 if the marker should be drawn as a line (with the thickness specified by the style)
60+
virtual int GetWidth() const { return 0; }
61+
5662
/// @brief Get the marker's drawing style
5763
/// @return A pen object describing the marker's drawing style
5864
virtual wxPen GetStyle() const = 0;
@@ -72,9 +78,12 @@ class AudioMarkerProvider {
7278

7379
~AudioMarkerProvider() = default;
7480
public:
75-
/// @brief Return markers in a time range
81+
/// @brief Return markers in a time range to render in audio display
7682
virtual void GetMarkers(const TimeRange &range, AudioMarkerVector &out) const = 0;
7783

84+
/// @brief Return markers in a time range to snap timed lines to
85+
virtual void GetSnapMarkers(const TimeRange &range, AudioMarkerVector &out) const { GetMarkers(range, out); };
86+
7887
DEFINE_SIGNAL_ADDERS(AnnounceMarkerMoved, AddMarkerMovedListener)
7988
};
8089

@@ -141,12 +150,16 @@ class AudioMarkerProviderKeyframes final : public AudioMarkerProvider {
141150
class VideoPositionMarkerProvider final : public AudioMarkerProvider {
142151
agi::Context *c;
143152

153+
std::unique_ptr<VideoPositionRange> range1;
154+
std::unique_ptr<VideoPositionRange> range2;
144155
std::unique_ptr<VideoPositionMarker> marker;
156+
std::unique_ptr<VideoPositionSnapPoint> snap1;
157+
std::unique_ptr<VideoPositionSnapPoint> snap2;
145158

146159
agi::signal::Connection video_seek_slot;
147160
agi::signal::Connection enable_opt_changed_slot;
148161

149-
void SetPosition(int frame_number);
162+
void SetPositions(int frame_number);
150163
void Update(int frame_number);
151164
void OptChanged(agi::OptionValue const& opt);
152165

@@ -155,6 +168,7 @@ class VideoPositionMarkerProvider final : public AudioMarkerProvider {
155168
~VideoPositionMarkerProvider();
156169

157170
void GetMarkers(const TimeRange &range, AudioMarkerVector &out) const override;
171+
void GetSnapMarkers(const TimeRange &range, AudioMarkerVector &out) const override;
158172
};
159173

160174
/// Marker provider for lines every second

src/audio_timing_dialogue.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -445,8 +445,8 @@ void AudioTimingControllerDialogue::GetMarkers(const TimeRange &range, AudioMark
445445
boost::upper_bound(markers, range.end(), marker_ptr_cmp()),
446446
back_inserter(out_markers));
447447

448-
keyframes_provider.GetMarkers(range, out_markers);
449448
video_position_provider.GetMarkers(range, out_markers);
449+
keyframes_provider.GetMarkers(range, out_markers);
450450
}
451451

452452
void AudioTimingControllerDialogue::OnSelectedSetChanged()
@@ -897,8 +897,8 @@ int AudioTimingControllerDialogue::SnapMarkers(int snap_range, std::vector<Audio
897897

898898
snap_markers.clear();
899899
TimeRange range(pos - snap_range, pos + snap_range);
900-
keyframes_provider.GetMarkers(range, snap_markers);
901-
video_position_provider.GetMarkers(range, snap_markers);
900+
keyframes_provider.GetSnapMarkers(range, snap_markers);
901+
video_position_provider.GetSnapMarkers(range, snap_markers);
902902

903903
for (const auto marker : snap_markers)
904904
{

src/libresrc/default_config.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@
107107
"Line boundary End" : "rgb(0, 0, 216)",
108108
"Line boundary Start" : "rgb(216, 0, 0)",
109109
"Play Cursor" : "rgb(255,255,255)",
110+
"Current Frame Range" : "rgba(255,255,255,160)",
111+
"Previous Frame Range" : "rgba(255,255,255,200)",
110112
"Seconds Line" : "rgb(0,100,255)",
111113
"Spectrum" : "Icy Blue",
112114
"Syllable Boundaries" : "rgb(255,255,0)",

src/preferences.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,8 @@ void Interface_Colours(wxTreebook *book, Preferences *parent) {
283283

284284
auto audio = p->PageSizer(_("Audio Display"));
285285
p->OptionAdd(audio, _("Play cursor"), "Colour/Audio Display/Play Cursor");
286+
p->OptionAdd(audio, _("Current frame range"), "Colour/Audio Display/Current Frame Range", {.alpha = true});
287+
p->OptionAdd(audio, _("Previous frame range"), "Colour/Audio Display/Previous Frame Range", {.alpha = true});
286288
p->OptionAdd(audio, _("Line boundary start"), "Colour/Audio Display/Line boundary Start");
287289
p->OptionAdd(audio, _("Line boundary end"), "Colour/Audio Display/Line boundary End");
288290
p->OptionAdd(audio, _("Line boundary inactive line"), "Colour/Audio Display/Line Boundary Inactive Line");

src/preferences_base.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ wxControl *OptionPage::OptionAdd(PageSection section, const wxString &name, cons
161161
}
162162

163163
case agi::OptionType::Color: {
164-
auto cb = new ColourButton(section.box, wxSize(40,10), false, opt->GetColor());
164+
auto cb = new ColourButton(section.box, wxSize(40,10), kwargs.alpha, opt->GetColor());
165165
cb->Bind(EVT_COLOR, ColourUpdater(opt_name, parent));
166166
Add(section, name, cb);
167167
return cb;

src/preferences_base.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ struct OptionAddArgs {
3737
double min = 0;
3838
double max = INT_MAX;
3939
double inc = 1;
40+
bool alpha = false;
4041
};
4142

4243
class OptionPage : public wxScrolled<wxPanel> {

0 commit comments

Comments
 (0)