Write subtitles and closed captions to a test DCP in the hints thread,
[dcpomatic.git] / src / lib / hints.cc
1 /*
2     Copyright (C) 2016-2020 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 "ratio.h"
31 #include "audio_analysis.h"
32 #include "compose.hpp"
33 #include "util.h"
34 #include "cross.h"
35 #include "player.h"
36 #include "writer.h"
37 #include <dcp/cpl.h>
38 #include <dcp/raw_convert.h>
39 #include <dcp/reel.h>
40 #include <dcp/reel_closed_caption_asset.h>
41 #include <dcp/reel_subtitle_asset.h>
42 #include <boost/foreach.hpp>
43 #include <boost/algorithm/string.hpp>
44 #include <iostream>
45
46 #include "i18n.h"
47
48 using std::vector;
49 using std::string;
50 using std::pair;
51 using std::min;
52 using std::max;
53 using std::cout;
54 using boost::shared_ptr;
55 using boost::weak_ptr;
56 using boost::optional;
57 using boost::bind;
58 using namespace dcpomatic;
59 #if BOOST_VERSION >= 106100
60 using namespace boost::placeholders;
61 #endif
62
63
64 /* When checking to see if things are too big, we'll say they are if they
65  * are more than the target size minus this "slack."
66  */
67 #define SIZE_SLACK 4096
68
69
70 Hints::Hints (weak_ptr<const Film> film)
71         : WeakConstFilm (film)
72         , _writer (new Writer(film, weak_ptr<Job>(), true))
73         , _long_ccap (false)
74         , _overlap_ccap (false)
75         , _too_many_ccap_lines (false)
76         , _early_subtitle (false)
77         , _short_subtitle (false)
78         , _subtitles_too_close (false)
79         , _too_many_subtitle_lines (false)
80         , _long_subtitle (false)
81         , _stop (false)
82 {
83
84 }
85
86 void
87 Hints::start ()
88 {
89         _thread = boost::thread (bind(&Hints::thread, this));
90 }
91
92 Hints::~Hints ()
93 {
94         boost::this_thread::disable_interruption dis;
95
96         try {
97                 _stop = true;
98                 _thread.interrupt ();
99                 _thread.join ();
100         } catch (...) {}
101 }
102
103
104 void
105 Hints::check_few_audio_channels ()
106 {
107         if (film()->audio_channels() < 6) {
108                 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."));
109         }
110 }
111
112
113 void
114 Hints::check_upmixers ()
115 {
116         AudioProcessor const * ap = film()->audio_processor();
117         if (ap && (ap->id() == "stereo-5.1-upmix-a" || ap->id() == "stereo-5.1-upmix-b")) {
118                 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."));
119         }
120 }
121
122
123 void
124 Hints::check_incorrect_container ()
125 {
126         int narrower_than_scope = 0;
127         int scope = 0;
128         BOOST_FOREACH (shared_ptr<const Content> i, film()->content()) {
129                 if (i->video) {
130                         Ratio const * r = Ratio::nearest_from_ratio(i->video->scaled_size(film()->frame_size()).ratio());
131                         if (r && r->id() == "239") {
132                                 ++scope;
133                         } else if (r && r->id() != "239" && r->id() != "235" && r->id() != "190") {
134                                 ++narrower_than_scope;
135                         }
136                 }
137         }
138
139         string const film_container = film()->container()->id();
140
141         if (scope && !narrower_than_scope && film_container == "185") {
142                 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."));
143         }
144
145         if (!scope && narrower_than_scope && film_container == "239") {
146                 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."));
147         }
148 }
149
150
151 void
152 Hints::check_unusual_container ()
153 {
154         string const film_container = film()->container()->id();
155         if (film_container != "185" && film_container != "239" && film_container != "190") {
156                 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"));
157         }
158 }
159
160
161 void
162 Hints::check_high_j2k_bandwidth ()
163 {
164         if (film()->j2k_bandwidth() >= 245000000) {
165                 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."));
166         }
167 }
168
169
170 void
171 Hints::check_frame_rate ()
172 {
173         shared_ptr<const Film> f = film ();
174         switch (f->video_frame_rate()) {
175         case 24:
176                 /* Fine */
177                 break;
178         case 25:
179         {
180                 /* You might want to go to 24 */
181                 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);
182                 if (f->interop()) {
183                         base += "  ";
184                         base += _("If you do use 25fps you should change your DCP standard to SMPTE.");
185                 }
186                 hint (base);
187                 break;
188         }
189         case 30:
190                 /* 30fps: we can't really offer any decent solutions */
191                 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."));
192                 break;
193         case 48:
194         case 50:
195         case 60:
196                 /* You almost certainly want to go to half frame rate */
197                 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));
198                 break;
199         }
200 }
201
202
203 void
204 Hints::check_speed_up ()
205 {
206         optional<double> lowest_speed_up;
207         optional<double> highest_speed_up;
208         BOOST_FOREACH (shared_ptr<const Content> i, film()->content()) {
209                 double spu = film()->active_frame_rate_change(i->position()).speed_up;
210                 if (!lowest_speed_up || spu < *lowest_speed_up) {
211                         lowest_speed_up = spu;
212                 }
213                 if (!highest_speed_up || spu > *highest_speed_up) {
214                         highest_speed_up = spu;
215                 }
216         }
217
218         double worst_speed_up = 1;
219         if (highest_speed_up) {
220                 worst_speed_up = *highest_speed_up;
221         }
222         if (lowest_speed_up) {
223                 worst_speed_up = max (worst_speed_up, 1 / *lowest_speed_up);
224         }
225
226         if (worst_speed_up > 25.5/24.0) {
227                 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."));
228         }
229
230 }
231
232
233 void
234 Hints::check_big_font_files ()
235 {
236         bool big_font_files = false;
237         if (film()->interop ()) {
238                 BOOST_FOREACH (shared_ptr<Content> i, film()->content()) {
239                         BOOST_FOREACH (shared_ptr<TextContent> j, i->text) {
240                                 BOOST_FOREACH (shared_ptr<Font> k, j->fonts()) {
241                                         optional<boost::filesystem::path> const p = k->file ();
242                                         if (p && boost::filesystem::file_size(p.get()) >= (MAX_FONT_FILE_SIZE - SIZE_SLACK)) {
243                                                 big_font_files = true;
244                                         }
245                                 }
246                         }
247                 }
248         }
249
250         if (big_font_files) {
251                 hint (_("You have specified a font file which is larger than 640kB.  This is very likely to cause problems on playback."));
252         }
253 }
254
255
256 void
257 Hints::check_vob ()
258 {
259         int vob = 0;
260         BOOST_FOREACH (shared_ptr<const Content> i, film()->content()) {
261                 if (boost::algorithm::starts_with (i->path(0).filename().string(), "VTS_")) {
262                         ++vob;
263                 }
264         }
265
266         if (vob > 1) {
267                 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));
268         }
269 }
270
271
272 void
273 Hints::check_3d_in_2d ()
274 {
275         int three_d = 0;
276         BOOST_FOREACH (shared_ptr<const Content> i, film()->content()) {
277                 if (i->video && i->video->frame_type() != VIDEO_FRAME_TYPE_2D) {
278                         ++three_d;
279                 }
280         }
281
282         if (three_d > 0 && !film()->three_d()) {
283                 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.)"));
284         }
285 }
286
287
288 void
289 Hints::check_loudness ()
290 {
291         boost::filesystem::path path = film()->audio_analysis_path(film()->playlist());
292         if (boost::filesystem::exists (path)) {
293                 try {
294                         shared_ptr<AudioAnalysis> an (new AudioAnalysis (path));
295
296                         string ch;
297
298                         vector<AudioAnalysis::PeakTime> sample_peak = an->sample_peak ();
299                         vector<float> true_peak = an->true_peak ();
300
301                         for (size_t i = 0; i < sample_peak.size(); ++i) {
302                                 float const peak = max (sample_peak[i].peak, true_peak.empty() ? 0 : true_peak[i]);
303                                 float const peak_dB = linear_to_db(peak) + an->gain_correction(film()->playlist());
304                                 if (peak_dB > -3) {
305                                         ch += dcp::raw_convert<string> (short_audio_channel_name (i)) + ", ";
306                                 }
307                         }
308
309                         ch = ch.substr (0, ch.length() - 2);
310
311                         if (!ch.empty ()) {
312                                 hint (String::compose (
313                                               _("Your audio level is very high (on %1).  You should reduce the gain of your audio content."),
314                                               ch
315                                               )
316                                         );
317                         }
318                 } catch (OldFormatError& e) {
319                         /* The audio analysis is too old to load in; just skip this hint as if
320                            it had never been run.
321                         */
322                 }
323         }
324 }
325
326
327 static
328 bool
329 subtitle_mxf_too_big (shared_ptr<dcp::SubtitleAsset> asset)
330 {
331         return asset && asset->file() && boost::filesystem::file_size(*asset->file()) >= (MAX_TEXT_MXF_SIZE - SIZE_SLACK);
332 }
333
334
335 void
336 Hints::thread ()
337 {
338         shared_ptr<const Film> film = _film.lock ();
339         if (!film) {
340                 return;
341         }
342
343         ContentList content = film->content ();
344
345         check_big_font_files ();
346         check_few_audio_channels ();
347         check_upmixers ();
348         check_incorrect_container ();
349         check_unusual_container ();
350         check_high_j2k_bandwidth ();
351         check_frame_rate ();
352         check_speed_up ();
353         check_vob ();
354         check_3d_in_2d ();
355         check_loudness ();
356         check_ffec_and_ffmc_in_smpte_feature ();
357
358         emit (bind(boost::ref(Progress), _("Examining closed captions")));
359
360         shared_ptr<Player> player (new Player(film));
361         player->set_ignore_video ();
362         player->set_ignore_audio ();
363         player->Text.connect (bind(&Hints::text, this, _1, _2, _3, _4));
364
365         struct timeval last_pulse;
366         gettimeofday (&last_pulse, 0);
367
368         try {
369                 while (!player->pass()) {
370
371                         struct timeval now;
372                         gettimeofday (&now, 0);
373                         if ((seconds(now) - seconds(last_pulse)) > 1) {
374                                 if (_stop) {
375                                         break;
376                                 }
377                                 emit (bind (boost::ref(Pulse)));
378                                 last_pulse = now;
379                         }
380                 }
381         } catch (...) {
382                 store_current ();
383         }
384
385         _writer->write (player->get_subtitle_fonts());
386
387         bool ccap_xml_too_big = false;
388         bool ccap_mxf_too_big = false;
389         bool subs_mxf_too_big = false;
390
391         boost::filesystem::path dcp_dir = film->dir("hints") / dcpomatic::get_process_id();
392         boost::filesystem::remove_all (dcp_dir);
393         _writer->finish (film->dir("hints") / dcpomatic::get_process_id());
394         dcp::DCP dcp (dcp_dir);
395         dcp.read ();
396         DCPOMATIC_ASSERT (dcp.cpls().size() == 1);
397         BOOST_FOREACH (shared_ptr<dcp::Reel> reel, dcp.cpls().front()->reels()) {
398                 BOOST_FOREACH (shared_ptr<dcp::ReelClosedCaptionAsset> ccap, reel->closed_captions()) {
399                         if (ccap->asset() && ccap->asset()->xml_as_string().length() > static_cast<size_t>(MAX_CLOSED_CAPTION_XML_SIZE - SIZE_SLACK) && !ccap_xml_too_big) {
400                                 hint (_(
401                                                 "At least one of your closed caption files' XML part is larger than " MAX_CLOSED_CAPTION_XML_SIZE_TEXT
402                                                 ".  You should divide the DCP into shorter reels."
403                                        ));
404                                 ccap_xml_too_big = true;
405                         }
406                         if (subtitle_mxf_too_big(ccap->asset()) && !ccap_mxf_too_big) {
407                                 hint (_(
408                                                 "At least one of your closed caption files is larger than " MAX_TEXT_MXF_SIZE_TEXT
409                                                 " in total.  You should divide the DCP into shorter reels."
410                                        ));
411                                 ccap_mxf_too_big = true;
412                         }
413                 }
414                 if (reel->main_subtitle() && subtitle_mxf_too_big(reel->main_subtitle()->asset()) && !subs_mxf_too_big) {
415                         hint (_(
416                                         "At least one of your subtitle files is larger than " MAX_TEXT_MXF_SIZE_TEXT " in total.  "
417                                         "You should divide the DCP into shorter reels."
418                                ));
419                         subs_mxf_too_big = true;
420                 }
421         }
422         boost::filesystem::remove_all (dcp_dir);
423
424         emit (bind(boost::ref(Finished)));
425 }
426
427 void
428 Hints::hint (string h)
429 {
430         emit(bind(boost::ref(Hint), h));
431 }
432
433 void
434 Hints::text (PlayerText text, TextType type, optional<DCPTextTrack> track, DCPTimePeriod period)
435 {
436         _writer->write (text, type, track, period);
437
438         switch (type) {
439         case TEXT_CLOSED_CAPTION:
440                 closed_caption (text, period);
441                 break;
442         case TEXT_OPEN_SUBTITLE:
443                 open_subtitle (text, period);
444                 break;
445         default:
446                 break;
447         }
448 }
449
450
451 void
452 Hints::closed_caption (PlayerText text, DCPTimePeriod period)
453 {
454         int lines = text.string.size();
455         BOOST_FOREACH (StringText i, text.string) {
456                 if (utf8_strlen(i.text()) > MAX_CLOSED_CAPTION_LENGTH) {
457                         ++lines;
458                         if (!_long_ccap) {
459                                 _long_ccap = true;
460                                 hint (
461                                         String::compose(
462                                                 "At least one of your closed caption lines has more than %1 characters.  "
463                                                 "It is advisable to make each line %1 characters at most in length.",
464                                                 MAX_CLOSED_CAPTION_LENGTH,
465                                                 MAX_CLOSED_CAPTION_LENGTH)
466                                      );
467                         }
468                 }
469         }
470
471         if (!_too_many_ccap_lines && lines > MAX_CLOSED_CAPTION_LINES) {
472                 hint (String::compose(_("Some of your closed captions span more than %1 lines, so they will be truncated."), MAX_CLOSED_CAPTION_LINES));
473                 _too_many_ccap_lines = true;
474         }
475
476         /* XXX: maybe overlapping closed captions (i.e. different languages) are OK with Interop? */
477         if (film()->interop() && !_overlap_ccap && _last_ccap && _last_ccap->overlap(period)) {
478                 _overlap_ccap = true;
479                 hint (_("You have overlapping closed captions, which are not allowed in Interop DCPs.  Change your DCP standard to SMPTE."));
480         }
481
482         _last_ccap = period;
483 }
484
485
486 void
487 Hints::open_subtitle (PlayerText text, DCPTimePeriod period)
488 {
489         if (period.from < DCPTime::from_seconds(4) && !_early_subtitle) {
490                 _early_subtitle = true;
491                 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."));
492         }
493
494         int const vfr = film()->video_frame_rate ();
495
496         if (period.duration().frames_round(vfr) < 15 && !_short_subtitle) {
497                 _short_subtitle = true;
498                 hint (_("At least one of your subtitles lasts less than 15 frames.  It is advisable to make each subtitle at least 15 frames long."));
499         }
500
501         if (_last_subtitle && DCPTime(period.from - _last_subtitle->to).frames_round(vfr) < 2 && !_subtitles_too_close) {
502                 _subtitles_too_close = true;
503                 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."));
504         }
505
506         if (text.string.size() > 3 && !_too_many_subtitle_lines) {
507                 _too_many_subtitle_lines = true;
508                 hint (_("At least one of your subtitles has more than 3 lines.  It is advisable to use no more than 3 lines."));
509         }
510
511         size_t longest_line = 0;
512         BOOST_FOREACH (StringText const& i, text.string) {
513                 longest_line = max (longest_line, i.text().length());
514         }
515
516         if (longest_line > 52 && !_long_subtitle) {
517                 _long_subtitle = true;
518                 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."));
519         }
520
521         _last_subtitle = period;
522 }
523
524
525 void
526 Hints::check_ffec_and_ffmc_in_smpte_feature ()
527 {
528         shared_ptr<const Film> f = film();
529         if (!f->interop() && f->dcp_content_type()->libdcp_kind() == dcp::FEATURE && (!f->marker(dcp::FFEC) || !f->marker(dcp::FFMC))) {
530                 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."));
531         }
532 }
533
534
535 void
536 Hints::join ()
537 {
538         _thread.join ();
539 }