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