Hint text tweaks.
[dcpomatic.git] / src / lib / hints.cc
1 /*
2     Copyright (C) 2016-2021 Carl Hetherington <cth@carlh.net>
3
4     This file is part of DCP-o-matic.
5
6     DCP-o-matic is free software; you can redistribute it and/or modify
7     it under the terms of the GNU General Public License as published by
8     the Free Software Foundation; either version 2 of the License, or
9     (at your option) any later version.
10
11     DCP-o-matic is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU General Public License for more details.
15
16     You should have received a copy of the GNU General Public License
17     along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
18
19 */
20
21 #include "dcp_content_type.h"
22 #include "hints.h"
23 #include "types.h"
24 #include "film.h"
25 #include "content.h"
26 #include "video_content.h"
27 #include "text_content.h"
28 #include "audio_processor.h"
29 #include "font.h"
30 #include "font_data.h"
31 #include "ratio.h"
32 #include "audio_analysis.h"
33 #include "compose.hpp"
34 #include "util.h"
35 #include "cross.h"
36 #include "player.h"
37 #include "writer.h"
38 #include <dcp/cpl.h>
39 #include <dcp/raw_convert.h>
40 #include <dcp/reel.h>
41 #include <dcp/reel_closed_caption_asset.h>
42 #include <dcp/reel_subtitle_asset.h>
43 #include <boost/algorithm/string.hpp>
44 #include <iostream>
45
46 #include "i18n.h"
47
48 using std::cout;
49 using std::make_shared;
50 using std::max;
51 using std::min;
52 using std::pair;
53 using std::shared_ptr;
54 using std::string;
55 using std::vector;
56 using std::weak_ptr;
57 using boost::optional;
58 using boost::bind;
59 using namespace dcpomatic;
60 #if BOOST_VERSION >= 106100
61 using namespace boost::placeholders;
62 #endif
63
64
65 /* When checking to see if things are too big, we'll say they are if they
66  * are more than the target size minus this "slack."
67  */
68 #define SIZE_SLACK 4096
69
70
71 /* When writing hints:
72  * - put quotation marks around the name of a GUI tab that you are referring to (e.g. "DCP" or "DCP→Video" tab)
73  */
74
75
76 Hints::Hints (weak_ptr<const Film> film)
77         : WeakConstFilm (film)
78         , _writer (new Writer(film, weak_ptr<Job>(), true))
79         , _long_ccap (false)
80         , _overlap_ccap (false)
81         , _too_many_ccap_lines (false)
82         , _early_subtitle (false)
83         , _short_subtitle (false)
84         , _subtitles_too_close (false)
85         , _too_many_subtitle_lines (false)
86         , _long_subtitle (false)
87         , _stop (false)
88 {
89
90 }
91
92 void
93 Hints::start ()
94 {
95         _thread = boost::thread (bind(&Hints::thread, this));
96 }
97
98 Hints::~Hints ()
99 {
100         boost::this_thread::disable_interruption dis;
101
102         try {
103                 _stop = true;
104                 _thread.interrupt ();
105                 _thread.join ();
106         } catch (...) {}
107 }
108
109
110 void
111 Hints::check_few_audio_channels ()
112 {
113         if (film()->audio_channels() < 6) {
114                 hint (_("Your DCP has fewer than 6 audio channels.  This may cause problems on some projectors.  You may want to set the DCP to have 6 channels.  It does not matter if your content has fewer channels, as DCP-o-matic will fill the extras with silence."));
115         }
116 }
117
118
119 void
120 Hints::check_upmixers ()
121 {
122         auto ap = film()->audio_processor();
123         if (ap && (ap->id() == "stereo-5.1-upmix-a" || ap->id() == "stereo-5.1-upmix-b")) {
124                 hint (_("You are using DCP-o-matic's stereo-to-5.1 upmixer.  This is experimental and may result in poor-quality audio.  If you continue, you should listen to the resulting DCP in a cinema to make sure that it sounds good."));
125         }
126 }
127
128
129 void
130 Hints::check_incorrect_container ()
131 {
132         int narrower_than_scope = 0;
133         int scope = 0;
134         for (auto i: film()->content()) {
135                 if (i->video) {
136                         Ratio const * r = Ratio::nearest_from_ratio(i->video->scaled_size(film()->frame_size()).ratio());
137                         if (r && r->id() == "239") {
138                                 ++scope;
139                         } else if (r && r->id() != "239" && r->id() != "235" && r->id() != "190") {
140                                 ++narrower_than_scope;
141                         }
142                 }
143         }
144
145         string const film_container = film()->container()->id();
146
147         if (scope && !narrower_than_scope && film_container == "185") {
148                 hint (_("All of your content is in Scope (2.39:1) but your DCP's container is Flat (1.85:1).  This will letter-box your content inside a Flat (1.85:1) frame.  You may prefer to set your DCP's container to Scope (2.39:1) in the \"DCP\" tab."));
149         }
150
151         if (!scope && narrower_than_scope && film_container == "239") {
152                 hint (_("All of your content narrower than 1.90:1 but your DCP's container is Scope (2.39:1).  This will pillar-box your content.  You may prefer to set your DCP's container to have the same ratio as your content."));
153         }
154 }
155
156
157 void
158 Hints::check_unusual_container ()
159 {
160         auto const film_container = film()->container()->id();
161         if (film_container != "185" && film_container != "239" && film_container != "190") {
162                 hint (_("Your DCP uses an unusual container ratio.  This may cause problems on some projectors.  If possible, use Flat or Scope for the DCP container ratio"));
163         }
164 }
165
166
167 void
168 Hints::check_high_j2k_bandwidth ()
169 {
170         if (film()->j2k_bandwidth() >= 245000000) {
171                 hint (_("A few projectors have problems playing back very high bit-rate DCPs.  It is a good idea to drop the JPEG2000 bandwidth down to about 200Mbit/s; this is unlikely to have any visible effect on the image."));
172         }
173 }
174
175
176 void
177 Hints::check_frame_rate ()
178 {
179         auto f = film ();
180         switch (f->video_frame_rate()) {
181         case 24:
182                 /* Fine */
183                 break;
184         case 25:
185         {
186                 /* You might want to go to 24 */
187                 string base = String::compose(_("You are set up for a DCP at a frame rate of %1 fps.  This frame rate is not supported by all projectors.  You may want to consider changing your frame rate to %2 fps."), 25, 24);
188                 if (f->interop()) {
189                         base += "  ";
190                         base += _("If you do use 25fps you should change your DCP standard to SMPTE.");
191                 }
192                 hint (base);
193                 break;
194         }
195         case 30:
196                 /* 30fps: we can't really offer any decent solutions */
197                 hint (_("You are set up for a DCP frame rate of 30fps, which is not supported by all projectors.  Be aware that you may have compatibility problems."));
198                 break;
199         case 48:
200         case 50:
201         case 60:
202                 /* You almost certainly want to go to half frame rate */
203                 hint (String::compose(_("You are set up for a DCP at a frame rate of %1 fps.  This frame rate is not supported by all projectors.  You are advised to change the DCP frame rate to %2 fps."), f->video_frame_rate(), f->video_frame_rate() / 2));
204                 break;
205         }
206 }
207
208
209 void
210 Hints::check_speed_up ()
211 {
212         optional<double> lowest_speed_up;
213         optional<double> highest_speed_up;
214         for (auto i: film()->content()) {
215                 double spu = film()->active_frame_rate_change(i->position()).speed_up;
216                 if (!lowest_speed_up || spu < *lowest_speed_up) {
217                         lowest_speed_up = spu;
218                 }
219                 if (!highest_speed_up || spu > *highest_speed_up) {
220                         highest_speed_up = spu;
221                 }
222         }
223
224         double worst_speed_up = 1;
225         if (highest_speed_up) {
226                 worst_speed_up = *highest_speed_up;
227         }
228         if (lowest_speed_up) {
229                 worst_speed_up = max (worst_speed_up, 1 / *lowest_speed_up);
230         }
231
232         if (worst_speed_up > 25.5/24.0) {
233                 hint (_("There is a large difference between the frame rate of your DCP and that of some of your content.  This will cause your audio to play back at a much lower or higher pitch than it should.  You are advised to set your DCP frame rate to one closer to your content, provided that your target projection systems support your chosen DCP rate."));
234         }
235
236 }
237
238
239 void
240 Hints::check_interop ()
241 {
242         if (film()->interop()) {
243                 hint (_("In general it is now advisable to make SMPTE DCPs unless you have a particular reason to use Interop.  You are advised to set your DCP to use the SMPTE standard in the \"DCP\" tab."));
244         }
245 }
246
247
248 void
249 Hints::check_big_font_files ()
250 {
251         bool big_font_files = false;
252         if (film()->interop ()) {
253                 for (auto i: film()->content()) {
254                         for (auto j: i->text) {
255                                 for (auto k: j->fonts()) {
256                                         optional<boost::filesystem::path> const p = k->file ();
257                                         if (p && boost::filesystem::file_size(p.get()) >= (MAX_FONT_FILE_SIZE - SIZE_SLACK)) {
258                                                 big_font_files = true;
259                                         }
260                                 }
261                         }
262                 }
263         }
264
265         if (big_font_files) {
266                 hint (_("You have specified a font file which is larger than 640kB.  This is very likely to cause problems on playback."));
267         }
268 }
269
270
271 void
272 Hints::check_vob ()
273 {
274         int vob = 0;
275         for (auto i: film()->content()) {
276                 if (boost::algorithm::starts_with (i->path(0).filename().string(), "VTS_")) {
277                         ++vob;
278                 }
279         }
280
281         if (vob > 1) {
282                 hint (String::compose (_("You have %1 files that look like they are VOB files from DVD. You should join them to ensure smooth joins between the files."), vob));
283         }
284 }
285
286
287 void
288 Hints::check_3d_in_2d ()
289 {
290         int three_d = 0;
291         for (auto i: film()->content()) {
292                 if (i->video && i->video->frame_type() != VideoFrameType::TWO_D) {
293                         ++three_d;
294                 }
295         }
296
297         if (three_d > 0 && !film()->three_d()) {
298                 hint (_("You are using 3D content but your DCP is set to 2D.  Set the DCP to 3D if you want to play it back on a 3D system (e.g. Real-D, MasterImage etc.)"));
299         }
300 }
301
302
303 void
304 Hints::check_loudness ()
305 {
306         auto path = film()->audio_analysis_path(film()->playlist());
307         if (boost::filesystem::exists (path)) {
308                 try {
309                         auto an = make_shared<AudioAnalysis>(path);
310
311                         string ch;
312
313                         auto sample_peak = an->sample_peak ();
314                         auto true_peak = an->true_peak ();
315
316                         for (size_t i = 0; i < sample_peak.size(); ++i) {
317                                 float const peak = max (sample_peak[i].peak, true_peak.empty() ? 0 : true_peak[i]);
318                                 float const peak_dB = linear_to_db(peak) + an->gain_correction(film()->playlist());
319                                 if (peak_dB > -3) {
320                                         ch += dcp::raw_convert<string> (short_audio_channel_name (i)) + ", ";
321                                 }
322                         }
323
324                         ch = ch.substr (0, ch.length() - 2);
325
326                         if (!ch.empty ()) {
327                                 hint (String::compose (
328                                               _("Your audio level is very high (on %1).  You should reduce the gain of your audio content."),
329                                               ch
330                                               )
331                                         );
332                         }
333                 } catch (OldFormatError& e) {
334                         /* The audio analysis is too old to load in; just skip this hint as if
335                            it had never been run.
336                         */
337                 }
338         }
339 }
340
341
342 static
343 bool
344 subtitle_mxf_too_big (shared_ptr<dcp::SubtitleAsset> asset)
345 {
346         return asset && asset->file() && boost::filesystem::file_size(*asset->file()) >= (MAX_TEXT_MXF_SIZE - SIZE_SLACK);
347 }
348
349
350 void
351 Hints::check_out_of_range_markers ()
352 {
353         auto const length = film()->length();
354         for (auto const& i: film()->markers()) {
355                 if (i.second >= length) {
356                         hint (_("At least one marker comes after the end of the project and will be ignored."));
357                 }
358         }
359 }
360
361
362 void
363 Hints::thread ()
364 {
365         auto film = _film.lock ();
366         if (!film) {
367                 return;
368         }
369
370         auto content = film->content ();
371
372         check_interop ();
373         check_big_font_files ();
374         check_few_audio_channels ();
375         check_upmixers ();
376         check_incorrect_container ();
377         check_unusual_container ();
378         check_high_j2k_bandwidth ();
379         check_frame_rate ();
380         check_speed_up ();
381         check_vob ();
382         check_3d_in_2d ();
383         check_loudness ();
384         check_ffec_and_ffmc_in_smpte_feature ();
385         check_out_of_range_markers ();
386
387         emit (bind(boost::ref(Progress), _("Examining closed captions")));
388
389         auto player = make_shared<Player>(film);
390         player->set_ignore_video ();
391         player->set_ignore_audio ();
392         player->Text.connect (bind(&Hints::text, this, _1, _2, _3, _4));
393
394         struct timeval last_pulse;
395         gettimeofday (&last_pulse, 0);
396
397         try {
398                 while (!player->pass()) {
399
400                         struct timeval now;
401                         gettimeofday (&now, 0);
402                         if ((seconds(now) - seconds(last_pulse)) > 1) {
403                                 if (_stop) {
404                                         break;
405                                 }
406                                 emit (bind (boost::ref(Pulse)));
407                                 last_pulse = now;
408                         }
409                 }
410         } catch (...) {
411                 store_current ();
412         }
413
414         _writer->write (player->get_subtitle_fonts());
415
416         bool ccap_xml_too_big = false;
417         bool ccap_mxf_too_big = false;
418         bool subs_mxf_too_big = false;
419
420         boost::filesystem::path dcp_dir = film->dir("hints") / dcpomatic::get_process_id();
421         boost::filesystem::remove_all (dcp_dir);
422
423         try {
424                 _writer->finish (film->dir("hints") / dcpomatic::get_process_id());
425         } catch (...) {
426                 store_current ();
427                 emit (bind(boost::ref(Finished)));
428                 return;
429         }
430
431         dcp::DCP dcp (dcp_dir);
432         dcp.read ();
433         DCPOMATIC_ASSERT (dcp.cpls().size() == 1);
434         for (auto reel: dcp.cpls()[0]->reels()) {
435                 for (auto ccap: reel->closed_captions()) {
436                         if (ccap->asset() && ccap->asset()->xml_as_string().length() > static_cast<size_t>(MAX_CLOSED_CAPTION_XML_SIZE - SIZE_SLACK) && !ccap_xml_too_big) {
437                                 hint (_(
438                                                 "At least one of your closed caption files' XML part is larger than " MAX_CLOSED_CAPTION_XML_SIZE_TEXT
439                                                 ".  You should divide the DCP into shorter reels."
440                                        ));
441                                 ccap_xml_too_big = true;
442                         }
443                         if (subtitle_mxf_too_big(ccap->asset()) && !ccap_mxf_too_big) {
444                                 hint (_(
445                                                 "At least one of your closed caption files is larger than " MAX_TEXT_MXF_SIZE_TEXT
446                                                 " in total.  You should divide the DCP into shorter reels."
447                                        ));
448                                 ccap_mxf_too_big = true;
449                         }
450                 }
451                 if (reel->main_subtitle() && subtitle_mxf_too_big(reel->main_subtitle()->asset()) && !subs_mxf_too_big) {
452                         hint (_(
453                                         "At least one of your subtitle files is larger than " MAX_TEXT_MXF_SIZE_TEXT " in total.  "
454                                         "You should divide the DCP into shorter reels."
455                                ));
456                         subs_mxf_too_big = true;
457                 }
458         }
459         boost::filesystem::remove_all (dcp_dir);
460
461         emit (bind(boost::ref(Finished)));
462 }
463
464 void
465 Hints::hint (string h)
466 {
467         emit(bind(boost::ref(Hint), h));
468 }
469
470 void
471 Hints::text (PlayerText text, TextType type, optional<DCPTextTrack> track, DCPTimePeriod period)
472 {
473         _writer->write (text, type, track, period);
474
475         switch (type) {
476         case TextType::CLOSED_CAPTION:
477                 closed_caption (text, period);
478                 break;
479         case TextType::OPEN_SUBTITLE:
480                 open_subtitle (text, period);
481                 break;
482         default:
483                 break;
484         }
485 }
486
487
488 void
489 Hints::closed_caption (PlayerText text, DCPTimePeriod period)
490 {
491         int lines = text.string.size();
492         for (auto i: text.string) {
493                 if (utf8_strlen(i.text()) > MAX_CLOSED_CAPTION_LENGTH) {
494                         ++lines;
495                         if (!_long_ccap) {
496                                 _long_ccap = true;
497                                 hint (
498                                         String::compose(
499                                                 "At least one of your closed caption lines has more than %1 characters.  "
500                                                 "It is advisable to make each line %1 characters at most in length.",
501                                                 MAX_CLOSED_CAPTION_LENGTH,
502                                                 MAX_CLOSED_CAPTION_LENGTH)
503                                      );
504                         }
505                 }
506         }
507
508         if (!_too_many_ccap_lines && lines > MAX_CLOSED_CAPTION_LINES) {
509                 hint (String::compose(_("Some of your closed captions span more than %1 lines, so they will be truncated."), MAX_CLOSED_CAPTION_LINES));
510                 _too_many_ccap_lines = true;
511         }
512
513         /* XXX: maybe overlapping closed captions (i.e. different languages) are OK with Interop? */
514         if (film()->interop() && !_overlap_ccap && _last_ccap && _last_ccap->overlap(period)) {
515                 _overlap_ccap = true;
516                 hint (_("You have overlapping closed captions, which are not allowed in Interop DCPs.  Change your DCP standard to SMPTE."));
517         }
518
519         _last_ccap = period;
520 }
521
522
523 void
524 Hints::open_subtitle (PlayerText text, DCPTimePeriod period)
525 {
526         if (period.from < DCPTime::from_seconds(4) && !_early_subtitle) {
527                 _early_subtitle = true;
528                 hint (_("It is advisable to put your first subtitle at least 4 seconds after the start of the DCP to make sure it is seen."));
529         }
530
531         int const vfr = film()->video_frame_rate ();
532
533         if (period.duration().frames_round(vfr) < 15 && !_short_subtitle) {
534                 _short_subtitle = true;
535                 hint (_("At least one of your subtitles lasts less than 15 frames.  It is advisable to make each subtitle at least 15 frames long."));
536         }
537
538         if (_last_subtitle && DCPTime(period.from - _last_subtitle->to).frames_round(vfr) < 2 && !_subtitles_too_close) {
539                 _subtitles_too_close = true;
540                 hint (_("At least one of your subtitles starts less than 2 frames after the previous one.  It is advisable to make the gap between subtitles at least 2 frames."));
541         }
542
543         if (text.string.size() > 3 && !_too_many_subtitle_lines) {
544                 _too_many_subtitle_lines = true;
545                 hint (_("At least one of your subtitles has more than 3 lines.  It is advisable to use no more than 3 lines."));
546         }
547
548         size_t longest_line = 0;
549         for (auto const& i: text.string) {
550                 longest_line = max (longest_line, i.text().length());
551         }
552
553         if (longest_line > 52 && !_long_subtitle) {
554                 _long_subtitle = true;
555                 hint (_("At least one of your subtitle lines has more than 52 characters.  It is advisable to make each line 52 characters at most in length."));
556         }
557
558         _last_subtitle = period;
559 }
560
561
562 void
563 Hints::check_ffec_and_ffmc_in_smpte_feature ()
564 {
565         auto f = film();
566         if (!f->interop() && f->dcp_content_type()->libdcp_kind() == dcp::ContentKind::FEATURE && (!f->marker(dcp::Marker::FFEC) || !f->marker(dcp::Marker::FFMC))) {
567                 hint (_("SMPTE DCPs with the type FTR (feature) should have markers for the first frame of end credits (FFEC) and the first frame of moving credits (FFMC).  You should add these markers using the 'Markers' button in the \"DCP\" tab."));
568         }
569 }
570
571
572 void
573 Hints::join ()
574 {
575         _thread.join ();
576 }