Support MCA sound channel tags in MXF/CPL.
[libdcp.git] / test / test.cc
1 /*
2     Copyright (C) 2012-2020 Carl Hetherington <cth@carlh.net>
3
4     This file is part of libdcp.
5
6     libdcp 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     libdcp 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 libdcp.  If not, see <http://www.gnu.org/licenses/>.
18
19     In addition, as a special exception, the copyright holders give
20     permission to link the code of portions of this program with the
21     OpenSSL library under certain conditions as described in each
22     individual source file, and distribute linked combinations
23     including the two.
24
25     You must obey the GNU General Public License in all respects
26     for all of the code used other than OpenSSL.  If you modify
27     file(s) with this exception, you may extend this exception to your
28     version of the file(s), but you are not obligated to do so.  If you
29     do not wish to do so, delete this exception statement from your
30     version.  If you delete this exception statement from all source
31     files in the program, then also delete it here.
32 */
33
34 #define BOOST_TEST_DYN_LINK
35 #define BOOST_TEST_MODULE libdcp_test
36 #include "compose.hpp"
37 #include "cpl.h"
38 #include "dcp.h"
39 #include "file.h"
40 #include "interop_subtitle_asset.h"
41 #include "mono_picture_asset.h"
42 #include "picture_asset_writer.h"
43 #include "reel.h"
44 #include "reel_mono_picture_asset.h"
45 #include "reel_sound_asset.h"
46 #include "reel_closed_caption_asset.h"
47 #include "reel_subtitle_asset.h"
48 #include "sound_asset.h"
49 #include "sound_asset_writer.h"
50 #include "smpte_subtitle_asset.h"
51 #include "mono_picture_asset.h"
52 #include "openjpeg_image.h"
53 #include "j2k.h"
54 #include "picture_asset_writer.h"
55 #include "reel_mono_picture_asset.h"
56 #include "reel_asset.h"
57 #include "test.h"
58 #include "util.h"
59 #include <asdcp/KM_util.h>
60 #include <asdcp/KM_prng.h>
61 #include <sndfile.h>
62 #include <libxml++/libxml++.h>
63 #include <boost/test/unit_test.hpp>
64 #include <cstdio>
65 #include <iostream>
66
67 using std::string;
68 using std::min;
69 using std::list;
70 using boost::shared_ptr;
71 using boost::optional;
72
73
74 boost::filesystem::path private_test;
75 boost::filesystem::path xsd_test = "build/test/xsd with spaces";
76
77
78 struct TestConfig
79 {
80         TestConfig()
81         {
82                 dcp::init ();
83                 if (boost::unit_test::framework::master_test_suite().argc >= 2) {
84                         private_test = boost::unit_test::framework::master_test_suite().argv[1];
85                 }
86
87                 using namespace boost::filesystem;
88                 boost::system::error_code ec;
89                 remove_all (xsd_test, ec);
90                 boost::filesystem::create_directory (xsd_test);
91                 for (directory_iterator i = directory_iterator("xsd"); i != directory_iterator(); ++i) {
92                         copy_file (*i, xsd_test / i->path().filename());
93                 }
94         }
95 };
96
97 void
98 check_xml (xmlpp::Element* ref, xmlpp::Element* test, list<string> ignore_tags, bool ignore_whitespace)
99 {
100         BOOST_CHECK_EQUAL (ref->get_name (), test->get_name ());
101         BOOST_CHECK_EQUAL (ref->get_namespace_prefix (), test->get_namespace_prefix ());
102
103         if (find(ignore_tags.begin(), ignore_tags.end(), ref->get_name()) != ignore_tags.end()) {
104                 return;
105         }
106
107         xmlpp::Element::NodeList ref_children = ref->get_children ();
108         xmlpp::Element::NodeList test_children = test->get_children ();
109         BOOST_REQUIRE_MESSAGE (
110                 ref_children.size () == test_children.size (),
111                 "child counts of " << ref->get_name() << " differ; ref has " << ref_children.size() << ", test has " << test_children.size()
112                 );
113
114         xmlpp::Element::NodeList::iterator k = ref_children.begin ();
115         xmlpp::Element::NodeList::iterator l = test_children.begin ();
116         while (k != ref_children.end ()) {
117
118                 /* XXX: should be doing xmlpp::EntityReference, xmlpp::XIncludeEnd, xmlpp::XIncludeStart */
119
120                 xmlpp::Element* ref_el = dynamic_cast<xmlpp::Element*> (*k);
121                 xmlpp::Element* test_el = dynamic_cast<xmlpp::Element*> (*l);
122                 BOOST_CHECK ((ref_el && test_el) || (!ref_el && !test_el));
123                 if (ref_el && test_el) {
124                         check_xml (ref_el, test_el, ignore_tags, ignore_whitespace);
125                 }
126
127                 xmlpp::ContentNode* ref_cn = dynamic_cast<xmlpp::ContentNode*> (*k);
128                 xmlpp::ContentNode* test_cn = dynamic_cast<xmlpp::ContentNode*> (*l);
129                 BOOST_CHECK ((ref_cn && test_cn) || (!ref_cn && !test_cn));
130                 if (ref_cn && test_cn) {
131                         if (
132                                 !ignore_whitespace ||
133                                 ref_cn->get_content().find_first_not_of(" \t\r\n") != string::npos ||
134                                 test_cn->get_content().find_first_not_of(" \t\r\n") != string::npos) {
135
136                                 BOOST_CHECK_EQUAL (ref_cn->get_content(), test_cn->get_content ());
137                         }
138                 }
139
140                 ++k;
141                 ++l;
142         }
143
144         xmlpp::Element::AttributeList ref_attributes = ref->get_attributes ();
145         xmlpp::Element::AttributeList test_attributes = test->get_attributes ();
146         BOOST_CHECK_EQUAL (ref_attributes.size(), test_attributes.size ());
147
148         xmlpp::Element::AttributeList::const_iterator m = ref_attributes.begin();
149         xmlpp::Element::AttributeList::const_iterator n = test_attributes.begin();
150         while (m != ref_attributes.end ()) {
151                 BOOST_CHECK_EQUAL ((*m)->get_name(), (*n)->get_name());
152                 BOOST_CHECK_EQUAL ((*m)->get_value(), (*n)->get_value());
153
154                 ++m;
155                 ++n;
156         }
157 }
158
159 void
160 check_xml (string ref, string test, list<string> ignore)
161 {
162         xmlpp::DomParser* ref_parser = new xmlpp::DomParser ();
163         ref_parser->parse_memory (ref);
164         xmlpp::Element* ref_root = ref_parser->get_document()->get_root_node ();
165         xmlpp::DomParser* test_parser = new xmlpp::DomParser ();
166         test_parser->parse_memory (test);
167         xmlpp::Element* test_root = test_parser->get_document()->get_root_node ();
168
169         check_xml (ref_root, test_root, ignore);
170 }
171
172 void
173 check_file (boost::filesystem::path ref, boost::filesystem::path check)
174 {
175         uintmax_t N = boost::filesystem::file_size (ref);
176         BOOST_CHECK_EQUAL (N, boost::filesystem::file_size (check));
177         FILE* ref_file = dcp::fopen_boost (ref, "rb");
178         BOOST_CHECK (ref_file);
179         FILE* check_file = dcp::fopen_boost (check, "rb");
180         BOOST_CHECK (check_file);
181
182         int const buffer_size = 65536;
183         uint8_t* ref_buffer = new uint8_t[buffer_size];
184         uint8_t* check_buffer = new uint8_t[buffer_size];
185
186         string error;
187         error = "File " + check.string() + " differs from reference " + ref.string();
188
189         while (N) {
190                 uintmax_t this_time = min (uintmax_t (buffer_size), N);
191                 size_t r = fread (ref_buffer, 1, this_time, ref_file);
192                 BOOST_CHECK_EQUAL (r, this_time);
193                 r = fread (check_buffer, 1, this_time, check_file);
194                 BOOST_CHECK_EQUAL (r, this_time);
195
196                 BOOST_CHECK_MESSAGE (memcmp (ref_buffer, check_buffer, this_time) == 0, error);
197                 if (memcmp (ref_buffer, check_buffer, this_time)) {
198                         break;
199                 }
200
201                 N -= this_time;
202         }
203
204         delete[] ref_buffer;
205         delete[] check_buffer;
206
207         fclose (ref_file);
208         fclose (check_file);
209 }
210
211
212 RNGFixer::RNGFixer ()
213 {
214         Kumu::cth_test = true;
215         Kumu::FortunaRNG().Reset();
216 }
217
218
219 RNGFixer::~RNGFixer ()
220 {
221         Kumu::cth_test = false;
222 }
223
224
225 shared_ptr<dcp::MonoPictureAsset>
226 simple_picture (boost::filesystem::path path, string suffix)
227 {
228         dcp::MXFMetadata mxf_meta;
229         mxf_meta.company_name = "OpenDCP";
230         mxf_meta.product_name = "OpenDCP";
231         mxf_meta.product_version = "0.0.25";
232
233         shared_ptr<dcp::MonoPictureAsset> mp (new dcp::MonoPictureAsset (dcp::Fraction (24, 1), dcp::SMPTE));
234         mp->set_metadata (mxf_meta);
235         shared_ptr<dcp::PictureAssetWriter> picture_writer = mp->start_write (path / dcp::String::compose("video%1.mxf", suffix), false);
236         dcp::File j2c ("test/data/32x32_red_square.j2c");
237         for (int i = 0; i < 24; ++i) {
238                 picture_writer->write (j2c.data (), j2c.size ());
239         }
240         picture_writer->finalize ();
241
242         return mp;
243 }
244
245
246 shared_ptr<dcp::DCP>
247 make_simple (boost::filesystem::path path, int reels)
248 {
249         /* Some known metadata */
250         dcp::XMLMetadata xml_meta;
251         xml_meta.annotation_text = "A Test DCP";
252         xml_meta.issuer = "OpenDCP 0.0.25";
253         xml_meta.creator = "OpenDCP 0.0.25";
254         xml_meta.issue_date = "2012-07-17T04:45:18+00:00";
255         dcp::MXFMetadata mxf_meta;
256         mxf_meta.company_name = "OpenDCP";
257         mxf_meta.product_name = "OpenDCP";
258         mxf_meta.product_version = "0.0.25";
259
260         boost::filesystem::remove_all (path);
261         boost::filesystem::create_directories (path);
262         shared_ptr<dcp::DCP> d (new dcp::DCP (path));
263         shared_ptr<dcp::CPL> cpl (new dcp::CPL ("A Test DCP", dcp::FEATURE));
264         cpl->set_content_version_id ("urn:uuid:75ac29aa-42ac-1234-ecae-49251abefd11");
265         cpl->set_content_version_label_text ("content-version-label-text");
266         cpl->set_metadata (xml_meta);
267
268         for (int i = 0; i < reels; ++i) {
269                 string suffix = reels == 1 ? "" : dcp::String::compose("%1", i);
270
271                 shared_ptr<dcp::MonoPictureAsset> mp = simple_picture (path, suffix);
272
273                 shared_ptr<dcp::SoundAsset> ms (new dcp::SoundAsset (dcp::Fraction (24, 1), 48000, 1, dcp::SMPTE));
274                 ms->set_metadata (mxf_meta);
275                 shared_ptr<dcp::SoundAssetWriter> sound_writer = ms->start_write (path / dcp::String::compose("audio%1.mxf", suffix));
276
277                 SF_INFO info;
278                 info.format = 0;
279                 SNDFILE* sndfile = sf_open ("test/data/1s_24-bit_48k_silence.wav", SFM_READ, &info);
280                 BOOST_CHECK (sndfile);
281                 float buffer[4096*6];
282                 float* channels[1];
283                 channels[0] = buffer;
284                 while (true) {
285                         sf_count_t N = sf_readf_float (sndfile, buffer, 4096);
286                         sound_writer->write (channels, N);
287                         if (N < 4096) {
288                                 break;
289                         }
290                 }
291
292                 sound_writer->finalize ();
293
294                 cpl->add (shared_ptr<dcp::Reel> (
295                                   new dcp::Reel (
296                                           shared_ptr<dcp::ReelMonoPictureAsset>(new dcp::ReelMonoPictureAsset(mp, 0)),
297                                           shared_ptr<dcp::ReelSoundAsset>(new dcp::ReelSoundAsset(ms, 0))
298                                           )
299                                   ));
300         }
301
302         d->add (cpl);
303         return d;
304 }
305
306
307 shared_ptr<dcp::Subtitle>
308 simple_subtitle ()
309 {
310         return shared_ptr<dcp::Subtitle>(
311                 new dcp::SubtitleString(
312                         optional<string>(),
313                         false,
314                         false,
315                         false,
316                         dcp::Colour(255, 255, 255),
317                         42,
318                         1,
319                         dcp::Time(0, 0, 4, 0, 24),
320                         dcp::Time(0, 0, 8, 0, 24),
321                         0.5,
322                         dcp::HALIGN_CENTER,
323                         0.8,
324                         dcp::VALIGN_TOP,
325                         dcp::DIRECTION_LTR,
326                         "Hello world",
327                         dcp::NONE,
328                         dcp::Colour(255, 255, 255),
329                         dcp::Time(),
330                         dcp::Time()
331                         )
332                 );
333 }
334
335
336 shared_ptr<dcp::DCP>
337 make_simple_with_interop_subs (boost::filesystem::path path)
338 {
339         shared_ptr<dcp::DCP> dcp = make_simple (path);
340
341         shared_ptr<dcp::InteropSubtitleAsset> subs(new dcp::InteropSubtitleAsset());
342         subs->add (simple_subtitle());
343
344         boost::filesystem::create_directory (path / "subs");
345         dcp::Data data(4096);
346         data.write (path / "subs" / "font.ttf");
347         subs->add_font ("afont", path / "subs" / "font.ttf");
348         subs->write (path / "subs" / "subs.xml");
349
350         shared_ptr<dcp::ReelSubtitleAsset> reel_subs(new dcp::ReelSubtitleAsset(subs, dcp::Fraction(24, 1), 240, 0));
351         dcp->cpls().front()->reels().front()->add (reel_subs);
352
353         return dcp;
354 }
355
356
357 shared_ptr<dcp::DCP>
358 make_simple_with_smpte_subs (boost::filesystem::path path)
359 {
360         shared_ptr<dcp::DCP> dcp = make_simple (path);
361
362         shared_ptr<dcp::SMPTESubtitleAsset> subs(new dcp::SMPTESubtitleAsset());
363         subs->add (simple_subtitle());
364
365         dcp::Data data(4096);
366         subs->write (path / "subs.mxf");
367
368         shared_ptr<dcp::ReelSubtitleAsset> reel_subs(new dcp::ReelSubtitleAsset(subs, dcp::Fraction(24, 1), 240, 0));
369         dcp->cpls().front()->reels().front()->add (reel_subs);
370
371         return dcp;
372 }
373
374
375 shared_ptr<dcp::DCP>
376 make_simple_with_interop_ccaps (boost::filesystem::path path)
377 {
378         shared_ptr<dcp::DCP> dcp = make_simple (path);
379
380         shared_ptr<dcp::InteropSubtitleAsset> subs(new dcp::InteropSubtitleAsset());
381         subs->add (simple_subtitle());
382         subs->write (path / "ccap.xml");
383
384         shared_ptr<dcp::ReelClosedCaptionAsset> reel_caps(new dcp::ReelClosedCaptionAsset(subs, dcp::Fraction(24, 1), 240, 0));
385         dcp->cpls().front()->reels().front()->add (reel_caps);
386
387         return dcp;
388 }
389
390
391 shared_ptr<dcp::DCP>
392 make_simple_with_smpte_ccaps (boost::filesystem::path path)
393 {
394         shared_ptr<dcp::DCP> dcp = make_simple (path);
395
396         shared_ptr<dcp::SMPTESubtitleAsset> subs(new dcp::SMPTESubtitleAsset());
397         subs->add (simple_subtitle());
398         subs->write (path / "ccap.mxf");
399
400         shared_ptr<dcp::ReelClosedCaptionAsset> reel_caps(new dcp::ReelClosedCaptionAsset(subs, dcp::Fraction(24, 1), 240, 0));
401         dcp->cpls().front()->reels().front()->add (reel_caps);
402
403         return dcp;
404 }
405
406
407 shared_ptr<dcp::OpenJPEGImage>
408 black_image ()
409 {
410         shared_ptr<dcp::OpenJPEGImage> image(new dcp::OpenJPEGImage(dcp::Size(1998, 1080)));
411         int const pixels = 1998 * 1080;
412         for (int i = 0; i < 3; ++i) {
413                 memset (image->data(i), 0, pixels * sizeof(int));
414         }
415         return image;
416 }
417
418
419 shared_ptr<dcp::ReelAsset>
420 black_picture_asset (boost::filesystem::path dir, int frames)
421 {
422         shared_ptr<dcp::OpenJPEGImage> image = black_image ();
423         dcp::Data frame = dcp::compress_j2k (image, 100000000, 24, false, false);
424         BOOST_REQUIRE (frame.size() < 230000000 / (24 * 8));
425
426         shared_ptr<dcp::MonoPictureAsset> asset(new dcp::MonoPictureAsset(dcp::Fraction(24, 1), dcp::SMPTE));
427         boost::filesystem::create_directories (dir);
428         shared_ptr<dcp::PictureAssetWriter> writer = asset->start_write (dir / "pic.mxf", true);
429         for (int i = 0; i < frames; ++i) {
430                 writer->write (frame.data().get(), frame.size());
431         }
432         writer->finalize ();
433
434         return shared_ptr<dcp::ReelAsset>(new dcp::ReelMonoPictureAsset(asset, 0));
435 }
436
437
438 BOOST_GLOBAL_FIXTURE (TestConfig);