1 // Copyright (c) 2014, 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 "project.h"
18 
19 #include "ass_dialogue.h"
20 #include "ass_file.h"
21 #include "async_video_provider.h"
22 #include "audio_controller.h"
23 #include "audio_provider_factory.h"
24 #include "base_grid.h"
25 #include "charset_detect.h"
26 #include "compat.h"
27 #include "dialog_progress.h"
28 #include "dialogs.h"
29 #include "format.h"
30 #include "include/aegisub/context.h"
31 #include "include/aegisub/video_provider.h"
32 #include "mkv_wrap.h"
33 #include "options.h"
34 #include "selection_controller.h"
35 #include "subs_controller.h"
36 #include "utils.h"
37 #include "video_controller.h"
38 #include "video_display.h"
39 
40 #include <libaegisub/audio/provider.h>
41 #include <libaegisub/format_path.h>
42 #include <libaegisub/fs.h>
43 #include <libaegisub/keyframe.h>
44 #include <libaegisub/log.h>
45 #include <libaegisub/make_unique.h>
46 #include <libaegisub/path.h>
47 
48 #include <boost/algorithm/string/case_conv.hpp>
49 #include <boost/filesystem/operations.hpp>
50 #include <wx/msgdlg.h>
51 
Project(agi::Context * c)52 Project::Project(agi::Context *c) : context(c) {
53 	OPT_SUB("Audio/Cache/Type", &Project::ReloadAudio, this);
54 	OPT_SUB("Audio/Provider", &Project::ReloadAudio, this);
55 	OPT_SUB("Provider/Audio/FFmpegSource/Decode Error Handling", &Project::ReloadAudio, this);
56 	OPT_SUB("Provider/Avisynth/Allow Ancient", &Project::ReloadVideo, this);
57 	OPT_SUB("Provider/Avisynth/Memory Max", &Project::ReloadVideo, this);
58 	OPT_SUB("Provider/Video/FFmpegSource/Decoding Threads", &Project::ReloadVideo, this);
59 	OPT_SUB("Provider/Video/FFmpegSource/Unsafe Seeking", &Project::ReloadVideo, this);
60 	OPT_SUB("Subtitle/Provider", &Project::ReloadVideo, this);
61 	OPT_SUB("Video/Force BT.601", &Project::ReloadVideo, this);
62 	OPT_SUB("Video/Provider", &Project::ReloadVideo, this);
63 }
64 
~Project()65 Project::~Project() { }
66 
UpdateRelativePaths()67 void Project::UpdateRelativePaths() {
68 	context->ass->Properties.audio_file     = config::path->MakeRelative(audio_file, "?script").generic_string();
69 	context->ass->Properties.video_file     = config::path->MakeRelative(video_file, "?script").generic_string();
70 	context->ass->Properties.timecodes_file = config::path->MakeRelative(timecodes_file, "?script").generic_string();
71 	context->ass->Properties.keyframes_file = config::path->MakeRelative(keyframes_file, "?script").generic_string();
72 }
73 
ReloadAudio()74 void Project::ReloadAudio() {
75 	if (audio_provider)
76 		LoadAudio(audio_file);
77 }
78 
ReloadVideo()79 void Project::ReloadVideo() {
80 	if (video_provider) {
81 		DoLoadVideo(video_file);
82 		context->videoController->JumpToFrame(context->videoController->GetFrameN());
83 	}
84 }
85 
ShowError(wxString const & message)86 void Project::ShowError(wxString const& message) {
87 	wxMessageBox(message, "Error loading file", wxOK | wxICON_ERROR | wxCENTER, context->parent);
88 }
89 
ShowError(std::string const & message)90 void Project::ShowError(std::string const& message) {
91 	ShowError(to_wx(message));
92 }
93 
SetPath(agi::fs::path & var,const char * token,const char * mru,agi::fs::path const & value)94 void Project::SetPath(agi::fs::path& var, const char *token, const char *mru, agi::fs::path const& value) {
95 	var = value;
96 	if (*token)
97 		config::path->SetToken(token, value);
98 	if (*mru)
99 		config::mru->Add(mru, value);
100 	UpdateRelativePaths();
101 }
102 
DoLoadSubtitles(agi::fs::path const & path,std::string encoding,ProjectProperties & properties)103 bool Project::DoLoadSubtitles(agi::fs::path const& path, std::string encoding, ProjectProperties &properties) {
104 	try {
105 		if (encoding.empty())
106 			encoding = CharSetDetect::GetEncoding(path);
107 	}
108 	catch (agi::UserCancelException const&) {
109 		return false;
110 	}
111 	catch (agi::fs::FileNotFound const&) {
112 		config::mru->Remove("Subtitle", path);
113 		ShowError(path.string() + " not found.");
114 		return false;
115 	}
116 
117 	if (encoding != "binary") {
118 		// Try loading as timecodes and keyframes first since we can't
119 		// distinguish them based on filename alone, and just ignore failures
120 		// rather than trying to differentiate between malformed timecodes
121 		// files and things that aren't timecodes files at all
122 		try { DoLoadTimecodes(path); return false; } catch (...) { }
123 		try { DoLoadKeyframes(path); return false; } catch (...) { }
124 	}
125 
126 	try {
127 		properties = context->subsController->Load(path, encoding);
128 	}
129 	catch (agi::UserCancelException const&) { return false; }
130 	catch (agi::fs::FileNotFound const&) {
131 		config::mru->Remove("Subtitle", path);
132 		ShowError(path.string() + " not found.");
133 		return false;
134 	}
135 	catch (agi::Exception const& e) {
136 		ShowError(e.GetMessage());
137 		return false;
138 	}
139 	catch (std::exception const& e) {
140 		ShowError(std::string(e.what()));
141 		return false;
142 	}
143 	catch (...) {
144 		ShowError(wxString("Unknown error"));
145 		return false;
146 	}
147 
148 	Selection sel;
149 	AssDialogue *active_line = nullptr;
150 	if (!context->ass->Events.empty()) {
151 		int row = mid<int>(0, properties.active_row, context->ass->Events.size() - 1);
152 		active_line = &*std::next(context->ass->Events.begin(), row);
153 		sel.insert(active_line);
154 	}
155 	context->selectionController->SetSelectionAndActive(std::move(sel), active_line);
156 	context->subsGrid->ScrollTo(properties.scroll_position);
157 
158 	return true;
159 }
160 
LoadSubtitles(agi::fs::path path,std::string encoding,bool load_linked)161 void Project::LoadSubtitles(agi::fs::path path, std::string encoding, bool load_linked) {
162 	ProjectProperties properties;
163 	if (DoLoadSubtitles(path, encoding, properties) && load_linked)
164 		LoadUnloadFiles(properties);
165 }
166 
CloseSubtitles()167 void Project::CloseSubtitles() {
168 	context->subsController->Close();
169 	config::path->SetToken("?script", "");
170 	LoadUnloadFiles(context->ass->Properties);
171 	auto line = &*context->ass->Events.begin();
172 	context->selectionController->SetSelectionAndActive({line}, line);
173 }
174 
LoadUnloadFiles(ProjectProperties properties)175 void Project::LoadUnloadFiles(ProjectProperties properties) {
176 	auto load_linked = OPT_GET("App/Auto/Load Linked Files")->GetInt();
177 	if (!load_linked) return;
178 
179 	auto audio     = config::path->MakeAbsolute(properties.audio_file, "?script");
180 	auto video     = config::path->MakeAbsolute(properties.video_file, "?script");
181 	auto timecodes = config::path->MakeAbsolute(properties.timecodes_file, "?script");
182 	auto keyframes = config::path->MakeAbsolute(properties.keyframes_file, "?script");
183 
184 	if (video == video_file && audio == audio_file && keyframes == keyframes_file && timecodes == timecodes_file)
185 		return;
186 
187 	if (load_linked == 2) {
188 		wxString str = _("Do you want to load/unload the associated files?");
189 		str += "\n";
190 
191 		auto append_file = [&](agi::fs::path const& p, wxString const& unload, wxString const& load) {
192 			if (p.empty())
193 				str += "\n" + unload;
194 			else
195 				str += "\n" + agi::wxformat(load, p);
196 		};
197 
198 		if (audio != audio_file)
199 			append_file(audio, _("Unload audio"), _("Load audio file: %s"));
200 		if (video != video_file)
201 			append_file(video, _("Unload video"), _("Load video file: %s"));
202 		if (timecodes != timecodes_file)
203 			append_file(timecodes, _("Unload timecodes"), _("Load timecodes file: %s"));
204 		if (keyframes != keyframes_file)
205 			append_file(keyframes, _("Unload keyframes"), _("Load keyframes file: %s"));
206 
207 		if (wxMessageBox(str, _("(Un)Load files?"), wxYES_NO | wxCENTRE, context->parent) != wxYES)
208 			return;
209 	}
210 
211 	bool loaded_video = false;
212 	if (video != video_file) {
213 		if (video.empty())
214 			CloseVideo();
215 		else if ((loaded_video = DoLoadVideo(video))) {
216 			auto vc = context->videoController.get();
217 			vc->JumpToFrame(properties.video_position);
218 
219 			auto ar_mode = static_cast<AspectRatio>(properties.ar_mode);
220 			if (ar_mode == AspectRatio::Custom)
221 				vc->SetAspectRatio(properties.ar_value);
222 			else
223 				vc->SetAspectRatio(ar_mode);
224 			context->videoDisplay->SetZoom(properties.video_zoom);
225 		}
226 	}
227 
228 	if (!timecodes.empty()) LoadTimecodes(timecodes);
229 	if (!keyframes.empty()) LoadKeyframes(keyframes);
230 
231 	if (audio != audio_file) {
232 		if (audio.empty())
233 			CloseAudio();
234 		else
235 			DoLoadAudio(audio, false);
236 	}
237 	else if (loaded_video && OPT_GET("Video/Open Audio")->GetBool() && audio_file != video_file && video_provider->HasAudio())
238 		DoLoadAudio(video, true);
239 }
240 
DoLoadAudio(agi::fs::path const & path,bool quiet)241 void Project::DoLoadAudio(agi::fs::path const& path, bool quiet) {
242 	if (!progress)
243 		progress = new DialogProgress(context->parent);
244 
245 	try {
246 		try {
247 			audio_provider = GetAudioProvider(path, progress);
248 		}
249 		catch (agi::UserCancelException const&) { return; }
250 		catch (...) {
251 			config::mru->Remove("Audio", path);
252 			throw;
253 		}
254 	}
255 	catch (agi::fs::FileNotFound const& e) {
256 		return ShowError(_("The audio file was not found: ") + to_wx(e.GetMessage()));
257 	}
258 	catch (agi::AudioDataNotFound const& e) {
259 		if (quiet) {
260 			LOG_D("video/open/audio") << "File " << video_file << " has no audio data: " << e.GetMessage();
261 			return;
262 		}
263 		else
264 			return ShowError(_("None of the available audio providers recognised the selected file as containing audio data.\n\nThe following providers were tried:\n") + to_wx(e.GetMessage()));
265 	}
266 	catch (agi::AudioProviderError const& e) {
267 		return ShowError(_("None of the available audio providers have a codec available to handle the selected file.\n\nThe following providers were tried:\n") + to_wx(e.GetMessage()));
268 	}
269 	catch (agi::Exception const& e) {
270 		return ShowError(e.GetMessage());
271 	}
272 
273 	SetPath(audio_file, "?audio", "Audio", path);
274 	AnnounceAudioProviderModified(audio_provider.get());
275 }
276 
LoadAudio(agi::fs::path path)277 void Project::LoadAudio(agi::fs::path path) {
278 	DoLoadAudio(path, false);
279 }
280 
CloseAudio()281 void Project::CloseAudio() {
282 	AnnounceAudioProviderModified(nullptr);
283 	audio_provider.reset();
284 	SetPath(audio_file, "?audio", "", "");
285 }
286 
DoLoadVideo(agi::fs::path const & path)287 bool Project::DoLoadVideo(agi::fs::path const& path) {
288 	if (!progress)
289 		progress = new DialogProgress(context->parent);
290 
291 	try {
292 		auto old_matrix = context->ass->GetScriptInfo("YCbCr Matrix");
293 		video_provider = agi::make_unique<AsyncVideoProvider>(path, old_matrix, context->videoController.get(), progress);
294 	}
295 	catch (agi::UserCancelException const&) { return false; }
296 	catch (agi::fs::FileSystemError const& err) {
297 		config::mru->Remove("Video", path);
298 		ShowError(to_wx(err.GetMessage()));
299 		return false;
300 	}
301 	catch (VideoProviderError const& err) {
302 		ShowError(to_wx(err.GetMessage()));
303 		return false;
304 	}
305 
306 	AnnounceVideoProviderModified(video_provider.get());
307 
308 	UpdateVideoProperties(context->ass.get(), video_provider.get(), context->parent);
309 	video_provider->LoadSubtitles(context->ass.get());
310 
311 	timecodes = video_provider->GetFPS();
312 	keyframes = video_provider->GetKeyFrames();
313 
314 	timecodes_file.clear();
315 	keyframes_file.clear();
316 	SetPath(video_file, "?video", "Video", path);
317 
318 	std::string warning = video_provider->GetWarning();
319 	if (!warning.empty())
320 		wxMessageBox(to_wx(warning), "Warning", wxICON_WARNING | wxOK);
321 
322 	video_has_subtitles = false;
323 	if (agi::fs::HasExtension(path, "mkv"))
324 		video_has_subtitles = MatroskaWrapper::HasSubtitles(path);
325 
326 	AnnounceKeyframesModified(keyframes);
327 	AnnounceTimecodesModified(timecodes);
328 	return true;
329 }
330 
LoadVideo(agi::fs::path path)331 void Project::LoadVideo(agi::fs::path path) {
332 	if (path.empty()) return;
333 	if (!DoLoadVideo(path)) return;
334 	if (OPT_GET("Video/Open Audio")->GetBool() && audio_file != video_file && video_provider->HasAudio())
335 		DoLoadAudio(video_file, true);
336 
337 	double dar = video_provider->GetDAR();
338 	if (dar > 0)
339 		context->videoController->SetAspectRatio(dar);
340 	else
341 		context->videoController->SetAspectRatio(AspectRatio::Default);
342 	context->videoController->JumpToFrame(0);
343 }
344 
CloseVideo()345 void Project::CloseVideo() {
346 	AnnounceVideoProviderModified(nullptr);
347 	video_provider.reset();
348 	SetPath(video_file, "?video", "", "");
349 	video_has_subtitles = false;
350 	context->ass->Properties.ar_mode = 0;
351 	context->ass->Properties.ar_value = 0.0;
352 	context->ass->Properties.video_position = 0;
353 }
354 
DoLoadTimecodes(agi::fs::path const & path)355 void Project::DoLoadTimecodes(agi::fs::path const& path) {
356 	timecodes = agi::vfr::Framerate(path);
357 	SetPath(timecodes_file, "", "Timecodes", path);
358 	AnnounceTimecodesModified(timecodes);
359 }
360 
LoadTimecodes(agi::fs::path path)361 void Project::LoadTimecodes(agi::fs::path path) {
362 	try {
363 		DoLoadTimecodes(path);
364 	}
365 	catch (agi::fs::FileSystemError const& e) {
366 		ShowError(e.GetMessage());
367 		config::mru->Remove("Timecodes", path);
368 	}
369 	catch (agi::vfr::Error const& e) {
370 		ShowError("Failed to parse timecodes file: " + e.GetMessage());
371 		config::mru->Remove("Timecodes", path);
372 	}
373 }
374 
CloseTimecodes()375 void Project::CloseTimecodes() {
376 	timecodes = video_provider ? video_provider->GetFPS() : agi::vfr::Framerate{};
377 	SetPath(timecodes_file, "", "", "");
378 	AnnounceTimecodesModified(timecodes);
379 }
380 
DoLoadKeyframes(agi::fs::path const & path)381 void Project::DoLoadKeyframes(agi::fs::path const& path) {
382 	keyframes = agi::keyframe::Load(path);
383 	SetPath(keyframes_file, "", "Keyframes", path);
384 	AnnounceKeyframesModified(keyframes);
385 }
386 
LoadKeyframes(agi::fs::path path)387 void Project::LoadKeyframes(agi::fs::path path) {
388 	try {
389 		DoLoadKeyframes(path);
390 	}
391 	catch (agi::fs::FileSystemError const& e) {
392 		ShowError(e.GetMessage());
393 		config::mru->Remove("Keyframes", path);
394 	}
395 	catch (agi::keyframe::Error const& e) {
396 		ShowError("Failed to parse keyframes file: " + e.GetMessage());
397 		config::mru->Remove("Keyframes", path);
398 	}
399 }
400 
CloseKeyframes()401 void Project::CloseKeyframes() {
402 	keyframes = video_provider ? video_provider->GetKeyFrames() : std::vector<int>{};
403 	SetPath(keyframes_file, "", "", "");
404 	AnnounceKeyframesModified(keyframes);
405 }
406 
LoadList(std::vector<agi::fs::path> const & files)407 void Project::LoadList(std::vector<agi::fs::path> const& files) {
408 	// Keep these lists sorted
409 
410 	// Video formats
411 	const char *videoList[] = {
412 		".asf",
413 		".avi",
414 		".avs",
415 		".d2v",
416 		".m2ts",
417 		".m4v",
418 		".mkv",
419 		".mov",
420 		".mp4",
421 		".mpeg",
422 		".mpg",
423 		".ogm",
424 		".rm",
425 		".rmvb",
426 		".ts",
427 		".webm"
428 		".wmv",
429 		".y4m",
430 		".yuv"
431 	};
432 
433 	// Subtitle formats
434 	const char *subsList[] = {
435 		".ass",
436 		".srt",
437 		".ssa",
438 		".sub",
439 		".ttxt"
440 	};
441 
442 	// Audio formats
443 	const char *audioList[] = {
444 		".aac",
445 		".ac3",
446 		".ape",
447 		".dts",
448 		".flac",
449 		".m4a",
450 		".mka",
451 		".mp3",
452 		".ogg",
453 		".w64",
454 		".wav",
455 		".wma"
456 	};
457 
458 	auto search = [](const char **begin, const char **end, std::string const& str) {
459 		return std::binary_search(begin, end, str.c_str(), [](const char *a, const char *b) {
460 			return strcmp(a, b) < 0;
461 		});
462 	};
463 
464 	agi::fs::path audio, video, subs, timecodes, keyframes;
465 	for (auto file : files) {
466 		if (file.is_relative()) file = absolute(file);
467 		if (!agi::fs::FileExists(file)) continue;
468 
469 		auto ext = file.extension().string();
470 		boost::to_lower(ext);
471 
472 		// Could be subtitles, keyframes or timecodes, so try loading as each
473 		if (ext == ".txt" || ext == ".log") {
474 			if (timecodes.empty()) {
475 				try {
476 					DoLoadTimecodes(file);
477 					timecodes = file;
478 					continue;
479 				} catch (...) { }
480 			}
481 
482 			if (keyframes.empty()) {
483 				try {
484 					DoLoadKeyframes(file);
485 					keyframes = file;
486 					continue;
487 				} catch (...) { }
488 			}
489 
490 			if (subs.empty() && ext != ".log")
491 				subs = file;
492 			continue;
493 		}
494 
495 		if (subs.empty() && search(std::begin(subsList), std::end(subsList), ext))
496 			subs = file;
497 		if (video.empty() && search(std::begin(videoList), std::end(videoList), ext))
498 			video = file;
499 		if (audio.empty() && search(std::begin(audioList), std::end(audioList), ext))
500 			audio = file;
501 	}
502 
503 	ProjectProperties properties;
504 	if (!subs.empty()) {
505 		if (!DoLoadSubtitles(subs, "", properties))
506 			subs.clear();
507 	}
508 
509 	if (!video.empty() && DoLoadVideo(video)) {
510 		double dar = video_provider->GetDAR();
511 		if (dar > 0)
512 			context->videoController->SetAspectRatio(dar);
513 		else
514 			context->videoController->SetAspectRatio(AspectRatio::Default);
515 		context->videoController->JumpToFrame(0);
516 
517 		// We loaded these earlier, but loading video unloaded them
518 		// Non-Do version of Load in case they've vanished or changed between
519 		// then and now
520 		if (!timecodes.empty())
521 			LoadTimecodes(timecodes);
522 		if (!keyframes.empty())
523 			LoadKeyframes(keyframes);
524 	}
525 
526 	if (!audio.empty())
527 		DoLoadAudio(audio, false);
528 	else if (OPT_GET("Video/Open Audio")->GetBool() && audio_file != video_file)
529 		DoLoadAudio(video_file, true);
530 
531 	if (!subs.empty())
532 		LoadUnloadFiles(properties);
533 }
534