1 // Copyright (c) 2011, Thomas Goyne <plorkyeran@aegisub.org>
2 //
3 // Permission to use, copy, modify, and distribute this software for any
4 // purpose with or without fee is hereby granted, provided that the above
5 // copyright notice and this permission notice appear in all copies.
6 //
7 // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8 // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9 // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
10 // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11 // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
12 // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
13 // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
14 //
15 // Aegisub Project http://www.aegisub.org/
16 
17 #include <libaegisub/signal.h>
18 
19 #include "ass_dialogue.h"
20 #include "ass_file.h"
21 #include "ass_karaoke.h"
22 #include "audio_controller.h"
23 #include "audio_marker.h"
24 #include "audio_rendering_style.h"
25 #include "audio_timing.h"
26 #include "compat.h"
27 #include "include/aegisub/context.h"
28 #include "options.h"
29 #include "pen.h"
30 #include "selection_controller.h"
31 #include "utils.h"
32 
33 #include <libaegisub/make_unique.h>
34 
35 #include <boost/range/algorithm/copy.hpp>
36 #include <boost/range/adaptor/filtered.hpp>
37 #include <boost/range/adaptor/sliced.hpp>
38 #include <wx/intl.h>
39 
40 /// @class KaraokeMarker
41 /// @brief AudioMarker implementation for AudioTimingControllerKaraoke
42 class KaraokeMarker final : public AudioMarker {
43 	int position;
44 	Pen *pen = nullptr;
45 	FeetStyle style = Feet_None;
46 public:
47 
GetPosition() const48 	int GetPosition() const override { return position; }
GetStyle() const49 	wxPen GetStyle() const override { return *pen; }
GetFeet() const50 	FeetStyle GetFeet() const override { return style; }
51 
Move(int new_pos)52 	void Move(int new_pos) { position = new_pos; }
53 
KaraokeMarker(int position)54 	KaraokeMarker(int position) : position(position) { }
55 
KaraokeMarker(int position,Pen * pen,FeetStyle style)56 	KaraokeMarker(int position, Pen *pen, FeetStyle style)
57 	: position(position)
58 	, pen(pen)
59 	, style(style)
60 	{
61 	}
62 
operator int() const63 	operator int() const { return position; }
64 };
65 
66 /// @class AudioTimingControllerKaraoke
67 /// @brief Karaoke timing mode for timing subtitles
68 ///
69 /// Displays the active line with draggable markers between each pair of
70 /// adjacent syllables, along with the text of each syllable.
71 ///
72 /// This does not support \kt, as it inherently requires that the end time of
73 /// one syllable be the same as the start time of the next one.
74 class AudioTimingControllerKaraoke final : public AudioTimingController {
75 	std::vector<agi::signal::Connection> connections;
76 	agi::signal::Connection& file_changed_slot;
77 
78 	agi::Context *c;          ///< Project context
79 	AssDialogue *active_line; ///< Currently active line
80 	AssKaraoke *kara;         ///< Parsed karaoke model provided by karaoke controller
81 
82 	size_t cur_syl = 0; ///< Index of currently selected syllable in the line
83 
84 	/// Pen used for the mid-syllable markers
85 	Pen separator_pen{"Colour/Audio Display/Syllable Boundaries", "Audio/Line Boundaries Thickness", wxPENSTYLE_DOT};
86 	/// Pen used for the start-of-line marker
87 	Pen start_pen{"Colour/Audio Display/Line boundary Start", "Audio/Line Boundaries Thickness"};
88 	/// Pen used for the end-of-line marker
89 	Pen end_pen{"Colour/Audio Display/Line boundary End", "Audio/Line Boundaries Thickness"};
90 
91 	/// Immobile marker for the beginning of the line
92 	KaraokeMarker start_marker;
93 	/// Immobile marker for the end of the line
94 	KaraokeMarker end_marker;
95 	/// Mobile markers between each pair of syllables
96 	std::vector<KaraokeMarker> markers;
97 
98 	/// Marker provider for video keyframes
99 	AudioMarkerProviderKeyframes keyframes_provider;
100 
101 	/// Marker provider for video playback position
102 	VideoPositionMarkerProvider video_position_provider;
103 
104 	/// Labels containing the stripped text of each syllable
105 	std::vector<AudioLabel> labels;
106 
107 	 /// Should changes be automatically commited?
108 	bool auto_commit = OPT_GET("Audio/Auto/Commit")->GetBool();
109 	int commit_id = -1;   ///< Last commit id used for an autocommit
110 	bool pending_changes; ///< Are there any pending changes to be committed?
111 
112 	void DoCommit();
113 	void ApplyLead(bool announce_primary);
114 	int MoveMarker(KaraokeMarker *marker, int new_position);
115 	void AnnounceChanges(int syl);
116 
117 public:
118 	// AudioTimingController implementation
119 	void GetMarkers(const TimeRange &range, AudioMarkerVector &out_markers) const override;
GetWarningMessage() const120 	wxString GetWarningMessage() const override { return ""; }
121 	TimeRange GetIdealVisibleTimeRange() const override;
122 	void GetRenderingStyles(AudioRenderingStyleRanges &ranges) const override;
123 	TimeRange GetPrimaryPlaybackRange() const override;
124 	TimeRange GetActiveLineRange() const override;
125 	void GetLabels(const TimeRange &range, std::vector<AudioLabel> &out_labels) const override;
126 	void Next(NextMode mode) override;
127 	void Prev() override;
128 	void Commit() override;
129 	void Revert() override;
130 	void AddLeadIn() override;
131 	void AddLeadOut() override;
132 	void ModifyLength(int delta, bool shift_following) override;
133 	void ModifyStart(int delta) override;
134 	bool IsNearbyMarker(int ms, int sensitivity, bool) const override;
135 	std::vector<AudioMarker*> OnLeftClick(int ms, bool, bool, int sensitivity, int) override;
136 	std::vector<AudioMarker*> OnRightClick(int ms, bool, int, int) override;
137 	void OnMarkerDrag(std::vector<AudioMarker*> const& marker, int new_position, int) override;
138 
139 	AudioTimingControllerKaraoke(agi::Context *c, AssKaraoke *kara, agi::signal::Connection& file_changed);
140 };
141 
CreateKaraokeTimingController(agi::Context * c,AssKaraoke * kara,agi::signal::Connection & file_changed)142 std::unique_ptr<AudioTimingController> CreateKaraokeTimingController(agi::Context *c, AssKaraoke *kara, agi::signal::Connection& file_changed)
143 {
144 	return agi::make_unique<AudioTimingControllerKaraoke>(c, kara, file_changed);
145 }
146 
AudioTimingControllerKaraoke(agi::Context * c,AssKaraoke * kara,agi::signal::Connection & file_changed)147 AudioTimingControllerKaraoke::AudioTimingControllerKaraoke(agi::Context *c, AssKaraoke *kara, agi::signal::Connection& file_changed)
148 : file_changed_slot(file_changed)
149 , c(c)
150 , active_line(c->selectionController->GetActiveLine())
151 , kara(kara)
152 , start_marker(active_line->Start, &start_pen, AudioMarker::Feet_Right)
153 , end_marker(active_line->End, &end_pen, AudioMarker::Feet_Left)
154 , keyframes_provider(c, "Audio/Display/Draw/Keyframes in Karaoke Mode")
155 , video_position_provider(c)
156 {
157 	connections.push_back(kara->AddSyllablesChangedListener(&AudioTimingControllerKaraoke::Revert, this));
158 	connections.push_back(OPT_SUB("Audio/Auto/Commit", [=](agi::OptionValue const& opt) { auto_commit = opt.GetBool(); }));
159 
160 	keyframes_provider.AddMarkerMovedListener([=]{ AnnounceMarkerMoved(); });
161 	video_position_provider.AddMarkerMovedListener([=]{ AnnounceMarkerMoved(); });
162 
163 	Revert();
164 }
165 
Next(NextMode mode)166 void AudioTimingControllerKaraoke::Next(NextMode mode) {
167 	// Don't create new lines since it's almost never useful to k-time a line
168 	// before dialogue timing it
169 	if (mode != TIMING_UNIT)
170 		cur_syl = markers.size();
171 
172 	++cur_syl;
173 	if (cur_syl > markers.size()) {
174 		--cur_syl;
175 		c->selectionController->NextLine();
176 	}
177 	else {
178 		AnnounceUpdatedPrimaryRange();
179 		AnnounceUpdatedStyleRanges();
180 	}
181 
182 	c->audioController->PlayPrimaryRange();
183 }
184 
Prev()185 void AudioTimingControllerKaraoke::Prev() {
186 	if (cur_syl == 0) {
187 		AssDialogue *old_line = active_line;
188 		c->selectionController->PrevLine();
189 		if (old_line != active_line) {
190 			cur_syl = markers.size();
191 			AnnounceUpdatedPrimaryRange();
192 			AnnounceUpdatedStyleRanges();
193 		}
194 	}
195 	else {
196 		--cur_syl;
197 		AnnounceUpdatedPrimaryRange();
198 		AnnounceUpdatedStyleRanges();
199 	}
200 
201 	c->audioController->PlayPrimaryRange();
202 }
203 
GetRenderingStyles(AudioRenderingStyleRanges & ranges) const204 void AudioTimingControllerKaraoke::GetRenderingStyles(AudioRenderingStyleRanges &ranges) const
205 {
206 	TimeRange sr = GetPrimaryPlaybackRange();
207 	ranges.AddRange(sr.begin(), sr.end(), AudioStyle_Primary);
208 	ranges.AddRange(start_marker, end_marker, AudioStyle_Selected);
209 }
210 
GetPrimaryPlaybackRange() const211 TimeRange AudioTimingControllerKaraoke::GetPrimaryPlaybackRange() const {
212 	return TimeRange(
213 		cur_syl > 0 ? markers[cur_syl - 1] : start_marker,
214 		cur_syl < markers.size() ? markers[cur_syl] : end_marker);
215 }
216 
GetActiveLineRange() const217 TimeRange AudioTimingControllerKaraoke::GetActiveLineRange() const {
218 	return TimeRange(start_marker, end_marker);
219 }
220 
GetIdealVisibleTimeRange() const221 TimeRange AudioTimingControllerKaraoke::GetIdealVisibleTimeRange() const {
222 	return GetActiveLineRange();
223 }
224 
GetMarkers(TimeRange const & range,AudioMarkerVector & out) const225 void AudioTimingControllerKaraoke::GetMarkers(TimeRange const& range, AudioMarkerVector &out) const {
226 	size_t i;
227 	for (i = 0; i < markers.size() && markers[i] < range.begin(); ++i) ;
228 	for (; i < markers.size() && markers[i] < range.end(); ++i)
229 		out.push_back(&markers[i]);
230 
231 	if (range.contains(start_marker)) out.push_back(&start_marker);
232 	if (range.contains(end_marker)) out.push_back(&end_marker);
233 
234 	keyframes_provider.GetMarkers(range, out);
235 	video_position_provider.GetMarkers(range, out);
236 }
237 
DoCommit()238 void AudioTimingControllerKaraoke::DoCommit() {
239 	active_line->Text = kara->GetText();
240 	file_changed_slot.Block();
241 	commit_id = c->ass->Commit(_("karaoke timing"), AssFile::COMMIT_DIAG_TEXT, commit_id, active_line);
242 	file_changed_slot.Unblock();
243 	pending_changes = false;
244 }
245 
Commit()246 void AudioTimingControllerKaraoke::Commit() {
247 	if (!auto_commit && pending_changes)
248 		DoCommit();
249 }
250 
Revert()251 void AudioTimingControllerKaraoke::Revert() {
252 	active_line = c->selectionController->GetActiveLine();
253 
254 	cur_syl = 0;
255 	commit_id = -1;
256 	pending_changes = false;
257 
258 	start_marker.Move(active_line->Start);
259 	end_marker.Move(active_line->End);
260 
261 	markers.clear();
262 	labels.clear();
263 
264 	markers.reserve(kara->size());
265 	labels.reserve(kara->size());
266 
267 	for (auto it = kara->begin(); it != kara->end(); ++it) {
268 		if (it != kara->begin())
269 			markers.emplace_back(it->start_time, &separator_pen, AudioMarker::Feet_None);
270 		labels.push_back(AudioLabel{to_wx(it->text), TimeRange(it->start_time, it->start_time + it->duration)});
271 	}
272 
273 	AnnounceUpdatedPrimaryRange();
274 	AnnounceUpdatedStyleRanges();
275 	AnnounceMarkerMoved();
276 }
277 
AddLeadIn()278 void AudioTimingControllerKaraoke::AddLeadIn() {
279 	start_marker.Move(start_marker - OPT_GET("Audio/Lead/IN")->GetInt());
280 	labels.front().range = TimeRange(start_marker, labels.front().range.end());
281 	ApplyLead(cur_syl == 0);
282 }
283 
AddLeadOut()284 void AudioTimingControllerKaraoke::AddLeadOut() {
285 	end_marker.Move(end_marker + OPT_GET("Audio/Lead/OUT")->GetInt());
286 	labels.back().range = TimeRange(labels.back().range.begin(), end_marker);
287 	ApplyLead(cur_syl == markers.size());
288 }
289 
ApplyLead(bool announce_primary)290 void AudioTimingControllerKaraoke::ApplyLead(bool announce_primary) {
291 	active_line->Start = (int)start_marker;
292 	active_line->End = (int)end_marker;
293 	kara->SetLineTimes(start_marker, end_marker);
294 	if (!announce_primary)
295 		AnnounceUpdatedStyleRanges();
296 	AnnounceChanges(announce_primary ? cur_syl : cur_syl + 2);
297 }
298 
ModifyLength(int delta,bool shift_following)299 void AudioTimingControllerKaraoke::ModifyLength(int delta, bool shift_following) {
300 	if (cur_syl == markers.size()) return;
301 
302 	int cur, end, step;
303 	if (delta < 0) {
304 		cur = cur_syl;
305 		end = shift_following ? markers.size() : cur_syl + 1;
306 		step = 1;
307 	}
308 	else {
309 		cur = shift_following ? markers.size() - 1 : cur_syl;
310 		end = cur_syl - 1;
311 		step = -1;
312 	}
313 
314 	for (; cur != end; cur += step) {
315 		MoveMarker(&markers[cur], markers[cur] + delta * 10);
316 	}
317 	AnnounceChanges(cur_syl);
318 }
319 
ModifyStart(int delta)320 void AudioTimingControllerKaraoke::ModifyStart(int delta) {
321 	if (cur_syl == 0) return;
322 	MoveMarker(&markers[cur_syl - 1], markers[cur_syl - 1] + delta * 10);
323 	AnnounceChanges(cur_syl);
324 }
325 
IsNearbyMarker(int ms,int sensitivity,bool) const326 bool AudioTimingControllerKaraoke::IsNearbyMarker(int ms, int sensitivity, bool) const {
327 	TimeRange range(ms - sensitivity, ms + sensitivity);
328 	return any_of(markers.begin(), markers.end(), [&](KaraokeMarker const& km) {
329 		return range.contains(km);
330 	});
331 }
332 
333 template<typename Out, typename In>
copy_ptrs(In & vec,size_t start,size_t end)334 static std::vector<Out *> copy_ptrs(In &vec, size_t start, size_t end) {
335 	std::vector<Out *> ret;
336 	ret.reserve(end - start);
337 	for (; start < end; ++start)
338 		ret.push_back(&vec[start]);
339 	return ret;
340 }
341 
OnLeftClick(int ms,bool ctrl_down,bool,int sensitivity,int)342 std::vector<AudioMarker*> AudioTimingControllerKaraoke::OnLeftClick(int ms, bool ctrl_down, bool, int sensitivity, int) {
343 	TimeRange range(ms - sensitivity, ms + sensitivity);
344 
345 	size_t syl = distance(markers.begin(), lower_bound(markers.begin(), markers.end(), ms));
346 	if (syl < markers.size() && range.contains(markers[syl]))
347 		return copy_ptrs<AudioMarker>(markers, syl, ctrl_down ? markers.size() : syl + 1);
348 	if (syl > 0 && range.contains(markers[syl - 1]))
349 		return copy_ptrs<AudioMarker>(markers, syl - 1, ctrl_down ? markers.size() : syl);
350 
351 	cur_syl = syl;
352 
353 	AnnounceUpdatedPrimaryRange();
354 	AnnounceUpdatedStyleRanges();
355 
356 	return {};
357 }
358 
OnRightClick(int ms,bool,int,int)359 std::vector<AudioMarker*> AudioTimingControllerKaraoke::OnRightClick(int ms, bool, int, int) {
360 	cur_syl = distance(markers.begin(), lower_bound(markers.begin(), markers.end(), ms));
361 
362 	AnnounceUpdatedPrimaryRange();
363 	AnnounceUpdatedStyleRanges();
364 	c->audioController->PlayPrimaryRange();
365 
366 	return {};
367 }
368 
MoveMarker(KaraokeMarker * marker,int new_position)369 int AudioTimingControllerKaraoke::MoveMarker(KaraokeMarker *marker, int new_position) {
370 	// No rearranging of syllables allowed
371 	new_position = mid(
372 		marker == &markers.front() ? start_marker.GetPosition() : (marker - 1)->GetPosition(),
373 		new_position,
374 		marker == &markers.back() ? end_marker.GetPosition() : (marker + 1)->GetPosition());
375 
376 	if (new_position == marker->GetPosition())
377 		return -1;
378 
379 	marker->Move(new_position);
380 
381 	size_t syl = marker - &markers.front() + 1;
382 	kara->SetStartTime(syl, (new_position + 5) / 10 * 10);
383 
384 	labels[syl - 1].range = TimeRange(labels[syl - 1].range.begin(), new_position);
385 	labels[syl].range = TimeRange(new_position, labels[syl].range.end());
386 
387 	return syl;
388 }
389 
AnnounceChanges(int syl)390 void AudioTimingControllerKaraoke::AnnounceChanges(int syl) {
391 	if (syl < 0) return;
392 
393 	if (syl == cur_syl || syl == cur_syl + 1) {
394 		AnnounceUpdatedPrimaryRange();
395 		AnnounceUpdatedStyleRanges();
396 	}
397 	AnnounceMarkerMoved();
398 	AnnounceLabelChanged();
399 
400 	if (auto_commit)
401 		DoCommit();
402 	else {
403 		pending_changes = true;
404 		commit_id = -1;
405 	}
406 }
407 
OnMarkerDrag(std::vector<AudioMarker * > const & m,int new_position,int)408 void AudioTimingControllerKaraoke::OnMarkerDrag(std::vector<AudioMarker*> const& m, int new_position, int) {
409 	int old_position = m[0]->GetPosition();
410 	int syl = MoveMarker(static_cast<KaraokeMarker *>(m[0]), new_position);
411 	if (syl < 0) return;
412 
413 	if (m.size() > 1) {
414 		int delta = m[0]->GetPosition() - old_position;
415 		for (AudioMarker *marker : m | boost::adaptors::sliced(1, m.size()))
416 			MoveMarker(static_cast<KaraokeMarker *>(marker), marker->GetPosition() + delta);
417 		syl = cur_syl;
418 	}
419 
420 	AnnounceChanges(syl);
421 }
422 
GetLabels(TimeRange const & range,std::vector<AudioLabel> & out) const423 void AudioTimingControllerKaraoke::GetLabels(TimeRange const& range, std::vector<AudioLabel> &out) const {
424 	copy(labels | boost::adaptors::filtered([&](AudioLabel const& l) {
425 		return range.overlaps(l.range);
426 	}), back_inserter(out));
427 }
428