int64_t
-Time::as_editable_units (int tcr_) const
+Time::as_editable_units_floor (int tcr_) const
+{
+ return floor(int64_t(e) * double(tcr_) / tcr) + int64_t(s) * tcr_ + int64_t(m) * 60 * tcr_ + int64_t(h) * 60 * 60 * tcr_;
+}
+
+
+int64_t
+Time::as_editable_units_ceil (int tcr_) const
{
return ceil(int64_t(e) * double(tcr_) / tcr) + int64_t(s) * tcr_ + int64_t(m) * 60 * tcr_ + int64_t(h) * 60 * 60 * tcr_;
}
/** @return the total number of seconds that this time consists of */
double as_seconds () const;
+ /** @param tcr_ Timecode rate with which the return value should be counted
+ * @return the total number of editable units that this time consists of at the specified timecode rate, rounded down
+ * to the nearest editable unit. For example, as_editable_units_floor(24) returns the total time in frames at 24fps.
+ */
+ int64_t as_editable_units_floor (int tcr_) const;
+
/** @param tcr_ Timecode rate with which the return value should be counted
* @return the total number of editable units that this time consists of at the specified timecode rate, rounded up
- * to the nearest editable unit. For example, as_editable_units (24) returns the total time in frames at 24fps.
+ * to the nearest editable unit. For example, as_editable_units_ceil(24) returns the total time in frames at 24fps.
*/
- int64_t as_editable_units (int tcr_) const;
+ int64_t as_editable_units_ceil (int tcr_) const;
/** @param tcr_ New timecode rate
* @return A new Time which is this time at the spcified new timecode rate
for (auto const& i: _markers) {
auto m = ml->add_child("Marker");
m->add_child("Label")->add_child_text(marker_to_string(i.first));
- m->add_child("Offset")->add_child_text(raw_convert<string>(i.second.as_editable_units(tcr)));
+ m->add_child("Offset")->add_child_text(raw_convert<string>(i.second.as_editable_units_ceil(tcr)));
}
return asset;
}
/* Guess intrinsic duration */
- _intrinsic_duration = latest_subtitle_out().as_editable_units (_edit_rate.numerator / _edit_rate.denominator);
+ _intrinsic_duration = latest_subtitle_out().as_editable_units_ceil(_edit_rate.numerator / _edit_rate.denominator);
}
SMPTESubtitleAsset::add (shared_ptr<Subtitle> s)
{
SubtitleAsset::add (s);
- _intrinsic_duration = latest_subtitle_out().as_editable_units (_edit_rate.numerator / _edit_rate.denominator);
+ _intrinsic_duration = latest_subtitle_out().as_editable_units_ceil(_edit_rate.numerator / _edit_rate.denominator);
}
e->set_attribute ("FadeUpTime", _fade_up.rebase(context.time_code_rate).as_string(context.standard));
e->set_attribute ("FadeDownTime", _fade_down.rebase(context.time_code_rate).as_string(context.standard));
} else {
- e->set_attribute ("FadeUpTime", raw_convert<string> (_fade_up.as_editable_units(context.time_code_rate)));
- e->set_attribute ("FadeDownTime", raw_convert<string> (_fade_down.as_editable_units(context.time_code_rate)));
+ e->set_attribute ("FadeUpTime", raw_convert<string> (_fade_up.as_editable_units_ceil(context.time_code_rate)));
+ e->set_attribute ("FadeDownTime", raw_convert<string> (_fade_down.as_editable_units_ceil(context.time_code_rate)));
}
return e;
}
void
verify_text_timing (
vector<shared_ptr<Reel>> reels,
- optional<int> picture_frame_rate,
+ int edit_rate,
vector<VerificationNote>& notes,
std::function<bool (shared_ptr<Reel>)> check,
std::function<string (shared_ptr<Reel>)> xml,
auto too_short = false;
auto too_close = false;
auto too_early = false;
+ auto reel_overlap = false;
/* current reel start time (in editable units) */
int64_t reel_offset = 0;
- std::function<void (cxml::ConstNodePtr, int, int, bool)> parse;
- parse = [&parse, &last_out, &too_short, &too_close, &too_early, &reel_offset](cxml::ConstNodePtr node, int tcr, int pfr, bool first_reel) {
+ std::function<void (cxml::ConstNodePtr, optional<int>, optional<Time>, int, bool)> parse;
+ parse = [&parse, &last_out, &too_short, &too_close, &too_early, &reel_offset](cxml::ConstNodePtr node, optional<int> tcr, optional<Time> start_time, int er, bool first_reel) {
if (node->name() == "Subtitle") {
Time in (node->string_attribute("TimeIn"), tcr);
+ if (start_time) {
+ in -= *start_time;
+ }
Time out (node->string_attribute("TimeOut"), tcr);
- if (first_reel && in < Time(0, 0, 4, 0, tcr)) {
+ if (start_time) {
+ out -= *start_time;
+ }
+ if (first_reel && tcr && in < Time(0, 0, 4, 0, *tcr)) {
too_early = true;
}
auto length = out - in;
- if (length.as_editable_units(pfr) < 15) {
+ if (length.as_editable_units_ceil(er) < 15) {
too_short = true;
}
if (last_out) {
/* XXX: this feels dubious - is it really what Bv2.1 means? */
- auto distance = reel_offset + in.as_editable_units(pfr) - *last_out;
+ auto distance = reel_offset + in.as_editable_units_ceil(er) - *last_out;
if (distance >= 0 && distance < 2) {
too_close = true;
}
}
- last_out = reel_offset + out.as_editable_units(pfr);
+ last_out = reel_offset + out.as_editable_units_floor(er);
} else {
for (auto i: node->node_children()) {
- parse(i, tcr, pfr, first_reel);
+ parse(i, tcr, start_time, er, first_reel);
}
}
};
* read in by libdcp's parser.
*/
- auto doc = make_shared<cxml::Document>("SubtitleReel");
- doc->read_string (xml(reels[i]));
- auto const tcr = doc->number_child<int>("TimeCodeRate");
- parse (doc, tcr, picture_frame_rate.get_value_or(24), i == 0);
- reel_offset += duration(reels[i]);
+ shared_ptr<cxml::Document> doc;
+ optional<int> tcr;
+ optional<Time> start_time;
+ try {
+ doc = make_shared<cxml::Document>("SubtitleReel");
+ doc->read_string (xml(reels[i]));
+ tcr = doc->number_child<int>("TimeCodeRate");
+ auto start_time_string = doc->optional_string_child("StartTime");
+ if (start_time_string) {
+ start_time = Time(*start_time_string, tcr);
+ }
+ } catch (...) {
+ doc = make_shared<cxml::Document>("DCSubtitle");
+ doc->read_string (xml(reels[i]));
+ }
+ parse (doc, tcr, start_time, edit_rate, i == 0);
+ auto end = reel_offset + duration(reels[i]);
+ if (last_out && *last_out > end) {
+ reel_overlap = true;
+ }
+ reel_offset = end;
+ }
+
+ if (last_out && *last_out > reel_offset) {
+ reel_overlap = true;
}
if (too_early) {
VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_SPACING
});
}
+
+ if (reel_overlap) {
+ notes.push_back ({
+ VerificationNote::Type::ERROR, VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY
+ });
+ }
}
return;
}
- optional<int> picture_frame_rate;
- if (reels[0]->main_picture()) {
- picture_frame_rate = reels[0]->main_picture()->frame_rate().numerator;
- }
-
if (reels[0]->main_subtitle()) {
- verify_text_timing (reels, picture_frame_rate, notes,
+ verify_text_timing (reels, reels[0]->main_subtitle()->edit_rate().numerator, notes,
[](shared_ptr<Reel> reel) {
return static_cast<bool>(reel->main_subtitle());
},
}
for (auto i = 0U; i < reels[0]->closed_captions().size(); ++i) {
- verify_text_timing (reels, picture_frame_rate, notes,
+ verify_text_timing (reels, reels[0]->closed_captions()[i]->edit_rate().numerator, notes,
[i](shared_ptr<Reel> reel) {
return i < reel->closed_captions().size();
},
most_closed_captions = std::max (most_closed_captions, reel->closed_captions().size());
}
+ verify_text_timing (cpl->reels(), notes);
+
if (dcp->standard() == Standard::SMPTE) {
if (have_main_subtitle && have_no_main_subtitle) {
if (lfoc == markers_seen.end()) {
notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_LFOC});
} else {
- auto lfoc_time = lfoc->second.as_editable_units(lfoc->second.tcr);
+ auto lfoc_time = lfoc->second.as_editable_units_ceil(lfoc->second.tcr);
if (lfoc_time != (cpl->reels().back()->duration() - 1)) {
notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_LFOC, raw_convert<string>(lfoc_time)});
}
}
- verify_text_timing (cpl->reels(), notes);
-
LinesCharactersResult result;
for (auto reel: cpl->reels()) {
if (reel->main_subtitle() && reel->main_subtitle()->asset()) {
return "At least one subtitle lasts less than 15 frames.";
case VerificationNote::Code::INVALID_SUBTITLE_SPACING:
return "At least one pair of subtitles is separated by less than 2 frames.";
+ case VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY:
+ return "At least one subtitle extends outside of its reel.";
case VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT:
return "There are more than 3 subtitle lines in at least one place in the DCP.";
case VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH:
INVALID_SUBTITLE_DURATION,
/** At least one pair of subtitles are separated by less than the the minimum of 2 frames suggested by [Bv2.1_7.2.5] */
INVALID_SUBTITLE_SPACING,
+ /** A subtitle lasts for longer than the reel which contains it */
+ SUBTITLE_OVERLAPS_REEL_BOUNDARY,
/** There are more than 3 subtitle lines in at least one place [Bv2.1_7.2.7] */
INVALID_SUBTITLE_LINE_COUNT,
/** There are more than 52 characters in at least one subtitle line [Bv2.1_7.2.7] */
check_verify_result (vector<path> dir, vector<dcp::VerificationNote> test_notes)
{
auto notes = dcp::verify ({dir}, &stage, &progress, xsd_test);
+ dump_notes (notes);
BOOST_REQUIRE_EQUAL (notes.size(), test_notes.size());
for (auto i = 0U; i < notes.size(); ++i) {
BOOST_REQUIRE_EQUAL (notes[i], test_notes[i]);
prepare_directory (dir);
copy_file ("test/data/subs.mxf", dir / "subs.mxf");
auto asset = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.mxf");
- auto reel_asset = make_shared<dcp::ReelSubtitleAsset>(asset, dcp::Fraction(24, 1), 16 * 24, 0);
+ auto reel_asset = make_shared<dcp::ReelSubtitleAsset>(asset, dcp::Fraction(25, 1), 300 * 24, 0);
auto cpl = write_dcp_with_single_asset (dir, reel_asset);
check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
prepare_directory (dir);
copy_file ("test/data/broken_smpte.mxf", dir / "subs.mxf");
auto asset = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.mxf");
- auto reel_asset = make_shared<dcp::ReelSubtitleAsset>(asset, dcp::Fraction(24, 1), 16 * 24, 0);
+ auto reel_asset = make_shared<dcp::ReelSubtitleAsset>(asset, dcp::Fraction(24, 1), 300 * 24, 0);
auto cpl = write_dcp_with_single_asset (dir, reel_asset);
check_verify_result (
auto asset = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.mxf");
asset->_language = "wrong-andbad";
asset->write (dir / "subs.mxf");
- auto reel_asset = make_shared<dcp::ReelSubtitleAsset>(asset, dcp::Fraction(24, 1), 16 * 24, 0);
+ auto reel_asset = make_shared<dcp::ReelSubtitleAsset>(asset, dcp::Fraction(24, 1), 300 * 24, 0);
reel_asset->_language = "badlang";
auto cpl = write_dcp_with_single_asset (dir, reel_asset);
auto asset = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.mxf");
asset->_language = "wrong-andbad";
asset->write (dir / "subs.mxf");
- auto reel_asset = make_shared<dcp::ReelClosedCaptionAsset>(asset, dcp::Fraction(24, 1), 16 * 24, 0);
+ auto reel_asset = make_shared<dcp::ReelClosedCaptionAsset>(asset, dcp::Fraction(24, 1), 300 * 24, 0);
reel_asset->_language = "badlang";
auto cpl = write_dcp_with_single_asset (dir, reel_asset);
}
asset->set_language (dcp::LanguageTag("de-DE"));
asset->write (dir / "subs.mxf");
- auto reel_asset = make_shared<dcp::ReelClosedCaptionAsset>(asset, dcp::Fraction(24, 1), 16 * 24, 0);
+ auto reel_asset = make_shared<dcp::ReelClosedCaptionAsset>(asset, dcp::Fraction(24, 1), 2049 * 24, 0);
auto cpl = write_dcp_with_single_asset (dir, reel_asset);
check_verify_result (
}
+BOOST_AUTO_TEST_CASE (verify_subtitle_overlapping_reel_boundary)
+{
+ auto const dir = path("build/test/verify_subtitle_overlapping_reel_boundary");
+ prepare_directory (dir);
+ auto asset = make_shared<dcp::SMPTESubtitleAsset>();
+ asset->set_start_time (dcp::Time());
+ add_test_subtitle (asset, 0, 4 * 24);
+ asset->set_language (dcp::LanguageTag("de-DE"));
+ asset->write (dir / "subs.mxf");
+
+ auto reel_asset = make_shared<dcp::ReelSubtitleAsset>(asset, dcp::Fraction(24, 1), 3 * 24, 0);
+ auto cpl = write_dcp_with_single_asset (dir, reel_asset);
+ check_verify_result (
+ {dir},
+ {
+ { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
+ { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY },
+ { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
+ });
+
+}
+
+
BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_line_count1)
{
auto const dir = path ("build/test/invalid_subtitle_line_count1");