Add audio language configuration and pass it to libdcp.
[dcpomatic.git] / src / lib / reel_writer.cc
1 /*
2     Copyright (C) 2012-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 "reel_writer.h"
22 #include "film.h"
23 #include "cross.h"
24 #include "job.h"
25 #include "log.h"
26 #include "dcpomatic_log.h"
27 #include "digester.h"
28 #include "font.h"
29 #include "compose.hpp"
30 #include "config.h"
31 #include "audio_buffers.h"
32 #include "image.h"
33 #include <dcp/atmos_asset.h>
34 #include <dcp/atmos_asset_writer.h>
35 #include <dcp/mono_picture_asset.h>
36 #include <dcp/stereo_picture_asset.h>
37 #include <dcp/sound_asset.h>
38 #include <dcp/sound_asset_writer.h>
39 #include <dcp/reel.h>
40 #include <dcp/reel_atmos_asset.h>
41 #include <dcp/reel_mono_picture_asset.h>
42 #include <dcp/reel_stereo_picture_asset.h>
43 #include <dcp/reel_sound_asset.h>
44 #include <dcp/reel_subtitle_asset.h>
45 #include <dcp/reel_closed_caption_asset.h>
46 #include <dcp/reel_markers_asset.h>
47 #include <dcp/dcp.h>
48 #include <dcp/cpl.h>
49 #include <dcp/certificate_chain.h>
50 #include <dcp/interop_subtitle_asset.h>
51 #include <dcp/smpte_subtitle_asset.h>
52 #include <dcp/raw_convert.h>
53 #include <dcp/subtitle_image.h>
54 #include <boost/foreach.hpp>
55
56 #include "i18n.h"
57
58 using std::list;
59 using std::string;
60 using std::cout;
61 using std::exception;
62 using std::map;
63 using std::vector;
64 using boost::shared_ptr;
65 using boost::optional;
66 using boost::dynamic_pointer_cast;
67 using dcp::Data;
68 using dcp::raw_convert;
69 using namespace dcpomatic;
70
71 int const ReelWriter::_info_size = 48;
72
73 static dcp::MXFMetadata
74 mxf_metadata ()
75 {
76         dcp::MXFMetadata meta;
77         Config* config = Config::instance();
78         if (!config->dcp_company_name().empty()) {
79                 meta.company_name = config->dcp_company_name ();
80         }
81         if (!config->dcp_product_name().empty()) {
82                 meta.product_name = config->dcp_product_name ();
83         }
84         if (!config->dcp_product_version().empty()) {
85                 meta.product_version = config->dcp_product_version ();
86         }
87         return meta;
88 }
89
90 /** @param job Related job, or 0 */
91 ReelWriter::ReelWriter (
92         shared_ptr<const Film> film, DCPTimePeriod period, shared_ptr<Job> job, int reel_index, int reel_count, optional<string> content_summary
93         )
94         : _film (film)
95         , _period (period)
96         , _reel_index (reel_index)
97         , _reel_count (reel_count)
98         , _content_summary (content_summary)
99         , _job (job)
100 {
101         /* Create or find our picture asset in a subdirectory, named
102            according to those film's parameters which affect the video
103            output.  We will hard-link it into the DCP later.
104         */
105
106         dcp::Standard const standard = _film->interop() ? dcp::INTEROP : dcp::SMPTE;
107
108         boost::filesystem::path const asset =
109                 _film->internal_video_asset_dir() / _film->internal_video_asset_filename(_period);
110
111         _first_nonexistant_frame = check_existing_picture_asset (asset);
112
113         if (_first_nonexistant_frame < period.duration().frames_round(_film->video_frame_rate())) {
114                 /* We do not have a complete picture asset.  If there is an
115                    existing asset, break any hard links to it as we are about
116                    to change its contents (if only by changing the IDs); see
117                    #1126.
118                 */
119                 if (boost::filesystem::exists(asset) && boost::filesystem::hard_link_count(asset) > 1) {
120                         if (job) {
121                                 job->sub (_("Copying old video file"));
122                                 copy_in_bits (asset, asset.string() + ".tmp", bind(&Job::set_progress, job.get(), _1, false));
123                         } else {
124                                 boost::filesystem::copy_file (asset, asset.string() + ".tmp");
125                         }
126                         boost::filesystem::remove (asset);
127                         boost::filesystem::rename (asset.string() + ".tmp", asset);
128                 }
129
130
131                 if (_film->three_d ()) {
132                         _picture_asset.reset (new dcp::StereoPictureAsset(dcp::Fraction(_film->video_frame_rate(), 1), standard));
133                 } else {
134                         _picture_asset.reset (new dcp::MonoPictureAsset(dcp::Fraction(_film->video_frame_rate(), 1), standard));
135                 }
136
137                 _picture_asset->set_size (_film->frame_size());
138                 _picture_asset->set_metadata (mxf_metadata());
139
140                 if (_film->encrypted ()) {
141                         _picture_asset->set_key (_film->key());
142                         _picture_asset->set_context_id (_film->context_id());
143                 }
144
145                 _picture_asset->set_file (asset);
146                 _picture_asset_writer = _picture_asset->start_write (asset, _first_nonexistant_frame > 0);
147         } else {
148                 /* We already have a complete picture asset that we can just re-use */
149                 /* XXX: what about if the encryption key changes? */
150                 if (_film->three_d ()) {
151                         _picture_asset.reset (new dcp::StereoPictureAsset(asset));
152                 } else {
153                         _picture_asset.reset (new dcp::MonoPictureAsset(asset));
154                 }
155         }
156
157         if (_film->audio_channels ()) {
158                 _sound_asset.reset (
159                         new dcp::SoundAsset (dcp::Fraction(_film->video_frame_rate(), 1), _film->audio_frame_rate(), _film->audio_channels(), _film->audio_language(), standard)
160                         );
161
162                 _sound_asset->set_metadata (mxf_metadata());
163
164                 if (_film->encrypted ()) {
165                         _sound_asset->set_key (_film->key ());
166                 }
167
168                 DCPOMATIC_ASSERT (_film->directory());
169
170                 vector<dcp::Channel> active;
171                 BOOST_FOREACH (int i, _film->mapped_audio_channels()) {
172                         active.push_back (static_cast<dcp::Channel>(i));
173                 }
174
175                 /* Write the sound asset into the film directory so that we leave the creation
176                    of the DCP directory until the last minute.
177                 */
178                 _sound_asset_writer = _sound_asset->start_write (
179                         _film->directory().get() / audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary),
180                         active,
181                         _film->contains_atmos_content()
182                         );
183         }
184 }
185
186 /** @param frame reel-relative frame */
187 void
188 ReelWriter::write_frame_info (Frame frame, Eyes eyes, dcp::FrameInfo info) const
189 {
190         shared_ptr<InfoFileHandle> handle = _film->info_file_handle(_period, false);
191         dcpomatic_fseek (handle->get(), frame_info_position(frame, eyes), SEEK_SET);
192         checked_fwrite (&info.offset, sizeof(info.offset), handle->get(), handle->file());
193         checked_fwrite (&info.size, sizeof (info.size), handle->get(), handle->file());
194         checked_fwrite (info.hash.c_str(), info.hash.size(), handle->get(), handle->file());
195 }
196
197 dcp::FrameInfo
198 ReelWriter::read_frame_info (shared_ptr<InfoFileHandle> info, Frame frame, Eyes eyes) const
199 {
200         dcp::FrameInfo frame_info;
201         dcpomatic_fseek (info->get(), frame_info_position(frame, eyes), SEEK_SET);
202         checked_fread (&frame_info.offset, sizeof(frame_info.offset), info->get(), info->file());
203         checked_fread (&frame_info.size, sizeof(frame_info.size), info->get(), info->file());
204
205         char hash_buffer[33];
206         checked_fread (hash_buffer, 32, info->get(), info->file());
207         hash_buffer[32] = '\0';
208         frame_info.hash = hash_buffer;
209
210         return frame_info;
211 }
212
213 long
214 ReelWriter::frame_info_position (Frame frame, Eyes eyes) const
215 {
216         switch (eyes) {
217         case EYES_BOTH:
218                 return frame * _info_size;
219         case EYES_LEFT:
220                 return frame * _info_size * 2;
221         case EYES_RIGHT:
222                 return frame * _info_size * 2 + _info_size;
223         default:
224                 DCPOMATIC_ASSERT (false);
225         }
226
227         DCPOMATIC_ASSERT (false);
228 }
229
230 Frame
231 ReelWriter::check_existing_picture_asset (boost::filesystem::path asset)
232 {
233         shared_ptr<Job> job = _job.lock ();
234
235         if (job) {
236                 job->sub (_("Checking existing image data"));
237         }
238
239         /* Try to open the existing asset */
240         FILE* asset_file = fopen_boost (asset, "rb");
241         if (!asset_file) {
242                 LOG_GENERAL ("Could not open existing asset at %1 (errno=%2)", asset.string(), errno);
243                 return 0;
244         } else {
245                 LOG_GENERAL ("Opened existing asset at %1", asset.string());
246         }
247
248         shared_ptr<InfoFileHandle> info_file;
249
250         try {
251                 info_file = _film->info_file_handle (_period, true);
252         } catch (OpenFileError &) {
253                 LOG_GENERAL_NC ("Could not open film info file");
254                 fclose (asset_file);
255                 return 0;
256         }
257
258         /* Offset of the last dcp::FrameInfo in the info file */
259         int const n = (boost::filesystem::file_size(info_file->file()) / _info_size) - 1;
260         LOG_GENERAL ("The last FI is %1; info file is %2, info size %3", n, boost::filesystem::file_size(info_file->file()), _info_size);
261
262         Frame first_nonexistant_frame;
263         if (_film->three_d ()) {
264                 /* Start looking at the last left frame */
265                 first_nonexistant_frame = n / 2;
266         } else {
267                 first_nonexistant_frame = n;
268         }
269
270         while (!existing_picture_frame_ok(asset_file, info_file, first_nonexistant_frame) && first_nonexistant_frame > 0) {
271                 --first_nonexistant_frame;
272         }
273
274         if (!_film->three_d() && first_nonexistant_frame > 0) {
275                 /* If we are doing 3D we might have found a good L frame with no R, so only
276                    do this if we're in 2D and we've just found a good B(oth) frame.
277                 */
278                 ++first_nonexistant_frame;
279         }
280
281         LOG_GENERAL ("Proceeding with first nonexistant frame %1", first_nonexistant_frame);
282
283         fclose (asset_file);
284
285         return first_nonexistant_frame;
286 }
287
288 void
289 ReelWriter::write (optional<Data> encoded, Frame frame, Eyes eyes)
290 {
291         if (!_picture_asset_writer) {
292                 /* We're not writing any data */
293                 return;
294         }
295
296         dcp::FrameInfo fin = _picture_asset_writer->write (encoded->data().get (), encoded->size());
297         write_frame_info (frame, eyes, fin);
298         _last_written[eyes] = encoded;
299 }
300
301
302 void
303 ReelWriter::write (shared_ptr<const dcp::AtmosFrame> atmos, AtmosMetadata metadata)
304 {
305         if (!_atmos_asset) {
306                 _atmos_asset = metadata.create (dcp::Fraction(_film->video_frame_rate(), 1));
307                 if (_film->encrypted()) {
308                         _atmos_asset->set_key(_film->key());
309                 }
310                 _atmos_asset_writer = _atmos_asset->start_write (
311                         _film->directory().get() / atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary)
312                         );
313         }
314         _atmos_asset_writer->write (atmos);
315 }
316
317
318 void
319 ReelWriter::fake_write (int size)
320 {
321         if (!_picture_asset_writer) {
322                 /* We're not writing any data */
323                 return;
324         }
325
326         _picture_asset_writer->fake_write (size);
327 }
328
329 void
330 ReelWriter::repeat_write (Frame frame, Eyes eyes)
331 {
332         if (!_picture_asset_writer) {
333                 /* We're not writing any data */
334                 return;
335         }
336
337         dcp::FrameInfo fin = _picture_asset_writer->write (
338                 _last_written[eyes]->data().get(),
339                 _last_written[eyes]->size()
340                 );
341         write_frame_info (frame, eyes, fin);
342 }
343
344 void
345 ReelWriter::finish ()
346 {
347         if (_picture_asset_writer && !_picture_asset_writer->finalize ()) {
348                 /* Nothing was written to the picture asset */
349                 LOG_GENERAL ("Nothing was written to reel %1 of %2", _reel_index, _reel_count);
350                 _picture_asset.reset ();
351         }
352
353         if (_sound_asset_writer && !_sound_asset_writer->finalize ()) {
354                 /* Nothing was written to the sound asset */
355                 _sound_asset.reset ();
356         }
357
358         /* Hard-link any video asset file into the DCP */
359         if (_picture_asset) {
360                 DCPOMATIC_ASSERT (_picture_asset->file());
361                 boost::filesystem::path video_from = _picture_asset->file().get();
362                 boost::filesystem::path video_to;
363                 video_to /= _film->dir (_film->dcp_name());
364                 video_to /= video_asset_filename (_picture_asset, _reel_index, _reel_count, _content_summary);
365                 /* There may be an existing "to" file if we are recreating a DCP in the same place without
366                    changing any video.
367                 */
368                 boost::system::error_code ec;
369                 boost::filesystem::remove (video_to, ec);
370
371                 boost::filesystem::create_hard_link (video_from, video_to, ec);
372                 if (ec) {
373                         LOG_WARNING_NC ("Hard-link failed; copying instead");
374                         shared_ptr<Job> job = _job.lock ();
375                         if (job) {
376                                 job->sub (_("Copying video file into DCP"));
377                                 try {
378                                         copy_in_bits (video_from, video_to, bind(&Job::set_progress, job.get(), _1, false));
379                                 } catch (exception& e) {
380                                         LOG_ERROR ("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), e.what());
381                                         throw FileError (e.what(), video_from);
382                                 }
383                         } else {
384                                 boost::filesystem::copy_file (video_from, video_to, ec);
385                                 if (ec) {
386                                         LOG_ERROR ("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), ec.message());
387                                         throw FileError (ec.message(), video_from);
388                                 }
389                         }
390                 }
391
392                 _picture_asset->set_file (video_to);
393         }
394
395         /* Move the audio asset into the DCP */
396         if (_sound_asset) {
397                 boost::filesystem::path audio_to;
398                 audio_to /= _film->dir (_film->dcp_name ());
399                 string const aaf = audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary);
400                 audio_to /= aaf;
401
402                 boost::system::error_code ec;
403                 boost::filesystem::rename (_film->file (aaf), audio_to, ec);
404                 if (ec) {
405                         throw FileError (
406                                 String::compose (_("could not move audio asset into the DCP (%1)"), ec.value ()), aaf
407                                 );
408                 }
409
410                 _sound_asset->set_file (audio_to);
411         }
412
413         if (_atmos_asset) {
414                 _atmos_asset_writer->finalize ();
415                 boost::filesystem::path atmos_to;
416                 atmos_to /= _film->dir (_film->dcp_name());
417                 string const aaf = atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary);
418                 atmos_to /= aaf;
419
420                 boost::system::error_code ec;
421                 boost::filesystem::rename (_film->file(aaf), atmos_to, ec);
422                 if (ec) {
423                         throw FileError (
424                                 String::compose (_("could not move atmos asset into the DCP (%1)"), ec.value ()), aaf
425                                 );
426                 }
427
428                 _atmos_asset->set_file (atmos_to);
429         }
430 }
431
432 template <class T>
433 shared_ptr<T>
434 maybe_add_text (
435         shared_ptr<dcp::SubtitleAsset> asset,
436         int64_t picture_duration,
437         shared_ptr<dcp::Reel> reel,
438         list<ReferencedReelAsset> const & refs,
439         list<shared_ptr<Font> > const & fonts,
440         shared_ptr<const Film> film,
441         DCPTimePeriod period
442         )
443 {
444         Frame const period_duration = period.duration().frames_round(film->video_frame_rate());
445
446         shared_ptr<T> reel_asset;
447
448         if (asset) {
449                 /* Add the font to the subtitle content */
450                 BOOST_FOREACH (shared_ptr<Font> j, fonts) {
451                         asset->add_font (j->id(), j->file().get_value_or(default_font_file()));
452                 }
453
454                 if (dynamic_pointer_cast<dcp::InteropSubtitleAsset> (asset)) {
455                         boost::filesystem::path directory = film->dir (film->dcp_name ()) / asset->id ();
456                         boost::filesystem::create_directories (directory);
457                         asset->write (directory / ("sub_" + asset->id() + ".xml"));
458                 } else {
459                         /* All our assets should be the same length; use the picture asset length here
460                            as a reference to set the subtitle one.  We'll use the duration rather than
461                            the intrinsic duration; we don't care if the picture asset has been trimmed, we're
462                            just interested in its presentation length.
463                         */
464                         dynamic_pointer_cast<dcp::SMPTESubtitleAsset>(asset)->set_intrinsic_duration (picture_duration);
465
466                         asset->write (
467                                 film->dir(film->dcp_name()) / ("sub_" + asset->id() + ".mxf")
468                                 );
469                 }
470
471                 reel_asset.reset (
472                         new T (
473                                 asset,
474                                 dcp::Fraction (film->video_frame_rate(), 1),
475                                 picture_duration,
476                                 0
477                                 )
478                         );
479         } else {
480                 /* We don't have a subtitle asset of our own; hopefully we have one to reference */
481                 BOOST_FOREACH (ReferencedReelAsset j, refs) {
482                         shared_ptr<T> k = dynamic_pointer_cast<T> (j.asset);
483                         if (k && j.period == period) {
484                                 reel_asset = k;
485                                 /* If we have a hash for this asset in the CPL, assume that it is correct */
486                                 if (k->hash()) {
487                                         k->asset_ref()->set_hash (k->hash().get());
488                                 }
489                         }
490                 }
491         }
492
493         if (reel_asset) {
494                 if (reel_asset->actual_duration() != period_duration) {
495                         throw ProgrammingError (
496                                 __FILE__, __LINE__,
497                                 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
498                                 );
499                 }
500                 reel->add (reel_asset);
501         }
502
503         return reel_asset;
504 }
505
506 shared_ptr<dcp::Reel>
507 ReelWriter::create_reel (list<ReferencedReelAsset> const & refs, list<shared_ptr<Font> > const & fonts)
508 {
509         LOG_GENERAL ("create_reel for %1-%2; %3 of %4", _period.from.get(), _period.to.get(), _reel_index, _reel_count);
510
511         shared_ptr<dcp::Reel> reel (new dcp::Reel ());
512
513         shared_ptr<dcp::ReelPictureAsset> reel_picture_asset;
514
515         if (_picture_asset) {
516                 /* We have made a picture asset of our own.  Put it into the reel */
517                 shared_ptr<dcp::MonoPictureAsset> mono = dynamic_pointer_cast<dcp::MonoPictureAsset> (_picture_asset);
518                 if (mono) {
519                         reel_picture_asset.reset (new dcp::ReelMonoPictureAsset (mono, 0));
520                 }
521
522                 shared_ptr<dcp::StereoPictureAsset> stereo = dynamic_pointer_cast<dcp::StereoPictureAsset> (_picture_asset);
523                 if (stereo) {
524                         reel_picture_asset.reset (new dcp::ReelStereoPictureAsset (stereo, 0));
525                 }
526         } else {
527                 LOG_GENERAL ("no picture asset of our own; look through %1", refs.size());
528                 /* We don't have a picture asset of our own; hopefully we have one to reference */
529                 BOOST_FOREACH (ReferencedReelAsset j, refs) {
530                         shared_ptr<dcp::ReelPictureAsset> k = dynamic_pointer_cast<dcp::ReelPictureAsset> (j.asset);
531                         if (k) {
532                                 LOG_GENERAL ("candidate picture asset period is %1-%2", j.period.from.get(), j.period.to.get());
533                         }
534                         if (k && j.period == _period) {
535                                 reel_picture_asset = k;
536                         }
537                 }
538         }
539
540         Frame const period_duration = _period.duration().frames_round(_film->video_frame_rate());
541
542         DCPOMATIC_ASSERT (reel_picture_asset);
543         if (reel_picture_asset->duration() != period_duration) {
544                 throw ProgrammingError (
545                         __FILE__, __LINE__,
546                         String::compose ("%1 vs %2", reel_picture_asset->actual_duration(), period_duration)
547                         );
548         }
549         reel->add (reel_picture_asset);
550
551         /* If we have a hash for this asset in the CPL, assume that it is correct */
552         if (reel_picture_asset->hash()) {
553                 reel_picture_asset->asset_ref()->set_hash (reel_picture_asset->hash().get());
554         }
555
556         shared_ptr<dcp::ReelSoundAsset> reel_sound_asset;
557
558         if (_sound_asset) {
559                 /* We have made a sound asset of our own.  Put it into the reel */
560                 reel_sound_asset.reset (new dcp::ReelSoundAsset (_sound_asset, 0));
561         } else {
562                 LOG_GENERAL ("no sound asset of our own; look through %1", refs.size());
563                 /* We don't have a sound asset of our own; hopefully we have one to reference */
564                 BOOST_FOREACH (ReferencedReelAsset j, refs) {
565                         shared_ptr<dcp::ReelSoundAsset> k = dynamic_pointer_cast<dcp::ReelSoundAsset> (j.asset);
566                         if (k) {
567                                 LOG_GENERAL ("candidate sound asset period is %1-%2", j.period.from.get(), j.period.to.get());
568                         }
569                         if (k && j.period == _period) {
570                                 reel_sound_asset = k;
571                                 /* If we have a hash for this asset in the CPL, assume that it is correct */
572                                 if (k->hash()) {
573                                         k->asset_ref()->set_hash (k->hash().get());
574                                 }
575                         }
576                 }
577         }
578
579         DCPOMATIC_ASSERT (reel_sound_asset);
580         if (reel_sound_asset->actual_duration() != period_duration) {
581                 LOG_ERROR (
582                         "Reel sound asset has length %1 but reel period is %2",
583                         reel_sound_asset->actual_duration(),
584                         period_duration
585                         );
586                 if (reel_sound_asset->actual_duration() != period_duration) {
587                         throw ProgrammingError (
588                                 __FILE__, __LINE__,
589                                 String::compose ("%1 vs %2", reel_sound_asset->actual_duration(), period_duration)
590                                 );
591                 }
592
593         }
594         reel->add (reel_sound_asset);
595
596         maybe_add_text<dcp::ReelSubtitleAsset> (_subtitle_asset, reel_picture_asset->actual_duration(), reel, refs, fonts, _film, _period);
597         for (map<DCPTextTrack, shared_ptr<dcp::SubtitleAsset> >::const_iterator i = _closed_caption_assets.begin(); i != _closed_caption_assets.end(); ++i) {
598                 shared_ptr<dcp::ReelClosedCaptionAsset> a = maybe_add_text<dcp::ReelClosedCaptionAsset> (
599                         i->second, reel_picture_asset->actual_duration(), reel, refs, fonts, _film, _period
600                         );
601                 a->set_annotation_text (i->first.name);
602                 a->set_language (i->first.language);
603         }
604
605         map<dcp::Marker, DCPTime> markers = _film->markers ();
606         map<dcp::Marker, DCPTime> reel_markers;
607         for (map<dcp::Marker, DCPTime>::const_iterator i = markers.begin(); i != markers.end(); ++i) {
608                 if (_period.contains(i->second)) {
609                         reel_markers[i->first] = i->second;
610                 }
611         }
612
613         if (!reel_markers.empty ()) {
614                 shared_ptr<dcp::ReelMarkersAsset> ma (new dcp::ReelMarkersAsset(dcp::Fraction(_film->video_frame_rate(), 1), 0));
615                 for (map<dcp::Marker, DCPTime>::const_iterator i = reel_markers.begin(); i != reel_markers.end(); ++i) {
616                         int h, m, s, f;
617                         DCPTime relative = i->second - _period.from;
618                         relative.split (_film->video_frame_rate(), h, m, s, f);
619                         ma->set (i->first, dcp::Time(h, m, s, f, _film->video_frame_rate()));
620                 }
621                 reel->add (ma);
622         }
623
624         if (_atmos_asset) {
625                 reel->add (shared_ptr<dcp::ReelAtmosAsset>(new dcp::ReelAtmosAsset(_atmos_asset, 0)));
626         }
627
628         return reel;
629 }
630
631 void
632 ReelWriter::calculate_digests (boost::function<void (float)> set_progress)
633 {
634         if (_picture_asset) {
635                 _picture_asset->hash (set_progress);
636         }
637
638         if (_sound_asset) {
639                 _sound_asset->hash (set_progress);
640         }
641
642         if (_atmos_asset) {
643                 _atmos_asset->hash (set_progress);
644         }
645 }
646
647 Frame
648 ReelWriter::start () const
649 {
650         return _period.from.frames_floor (_film->video_frame_rate());
651 }
652
653
654 void
655 ReelWriter::write (shared_ptr<const AudioBuffers> audio)
656 {
657         if (!_sound_asset_writer) {
658                 return;
659         }
660
661         DCPOMATIC_ASSERT (audio);
662         _sound_asset_writer->write (audio->data(), audio->frames());
663 }
664
665 void
666 ReelWriter::write (PlayerText subs, TextType type, optional<DCPTextTrack> track, DCPTimePeriod period)
667 {
668         shared_ptr<dcp::SubtitleAsset> asset;
669
670         switch (type) {
671         case TEXT_OPEN_SUBTITLE:
672                 asset = _subtitle_asset;
673                 break;
674         case TEXT_CLOSED_CAPTION:
675                 DCPOMATIC_ASSERT (track);
676                 asset = _closed_caption_assets[*track];
677                 break;
678         default:
679                 DCPOMATIC_ASSERT (false);
680         }
681
682         if (!asset) {
683                 string lang = _film->subtitle_language ();
684                 if (_film->interop ()) {
685                         shared_ptr<dcp::InteropSubtitleAsset> s (new dcp::InteropSubtitleAsset ());
686                         s->set_movie_title (_film->name ());
687                         if (type == TEXT_OPEN_SUBTITLE) {
688                                 s->set_language (lang.empty() ? "Unknown" : lang);
689                         } else {
690                                 s->set_language (track->language);
691                         }
692                         s->set_reel_number (raw_convert<string> (_reel_index + 1));
693                         asset = s;
694                 } else {
695                         shared_ptr<dcp::SMPTESubtitleAsset> s (new dcp::SMPTESubtitleAsset ());
696                         s->set_content_title_text (_film->name ());
697                         s->set_metadata (mxf_metadata());
698                         if (type == TEXT_OPEN_SUBTITLE && !lang.empty()) {
699                                 s->set_language (lang);
700                         } else {
701                                 s->set_language (track->language);
702                         }
703                         s->set_edit_rate (dcp::Fraction (_film->video_frame_rate (), 1));
704                         s->set_reel_number (_reel_index + 1);
705                         s->set_time_code_rate (_film->video_frame_rate ());
706                         s->set_start_time (dcp::Time ());
707                         if (_film->encrypted ()) {
708                                 s->set_key (_film->key ());
709                         }
710                         asset = s;
711                 }
712         }
713
714         switch (type) {
715         case TEXT_OPEN_SUBTITLE:
716                 _subtitle_asset = asset;
717                 break;
718         case TEXT_CLOSED_CAPTION:
719                 DCPOMATIC_ASSERT (track);
720                 _closed_caption_assets[*track] = asset;
721                 break;
722         default:
723                 DCPOMATIC_ASSERT (false);
724         }
725
726         BOOST_FOREACH (StringText i, subs.string) {
727                 /* XXX: couldn't / shouldn't we use period here rather than getting time from the subtitle? */
728                 i.set_in  (i.in()  - dcp::Time (_period.from.seconds(), i.in().tcr));
729                 i.set_out (i.out() - dcp::Time (_period.from.seconds(), i.out().tcr));
730                 asset->add (shared_ptr<dcp::Subtitle>(new dcp::SubtitleString(i)));
731         }
732
733         BOOST_FOREACH (BitmapText i, subs.bitmap) {
734                 asset->add (
735                         shared_ptr<dcp::Subtitle>(
736                                 new dcp::SubtitleImage(
737                                         i.image->as_png(),
738                                         dcp::Time(period.from.seconds() - _period.from.seconds(), _film->video_frame_rate()),
739                                         dcp::Time(period.to.seconds() - _period.from.seconds(), _film->video_frame_rate()),
740                                         i.rectangle.x, dcp::HALIGN_LEFT, i.rectangle.y, dcp::VALIGN_TOP,
741                                         dcp::Time(), dcp::Time()
742                                         )
743                                 )
744                         );
745         }
746 }
747
748 bool
749 ReelWriter::existing_picture_frame_ok (FILE* asset_file, shared_ptr<InfoFileHandle> info_file, Frame frame) const
750 {
751         LOG_GENERAL ("Checking existing picture frame %1", frame);
752
753         /* Read the data from the info file; for 3D we just check the left
754            frames until we find a good one.
755         */
756         dcp::FrameInfo const info = read_frame_info (info_file, frame, _film->three_d () ? EYES_LEFT : EYES_BOTH);
757
758         bool ok = true;
759
760         /* Read the data from the asset and hash it */
761         dcpomatic_fseek (asset_file, info.offset, SEEK_SET);
762         Data data (info.size);
763         size_t const read = fread (data.data().get(), 1, data.size(), asset_file);
764         LOG_GENERAL ("Read %1 bytes of asset data; wanted %2", read, info.size);
765         if (read != static_cast<size_t> (data.size ())) {
766                 LOG_GENERAL ("Existing frame %1 is incomplete", frame);
767                 ok = false;
768         } else {
769                 Digester digester;
770                 digester.add (data.data().get(), data.size());
771                 LOG_GENERAL ("Hash %1 vs %2", digester.get(), info.hash);
772                 if (digester.get() != info.hash) {
773                         LOG_GENERAL ("Existing frame %1 failed hash check", frame);
774                         ok = false;
775                 }
776         }
777
778         return ok;
779 }