From 42f8538a70350babd387d7d2aa3052221bb45316 Mon Sep 17 00:00:00 2001 From: Moritz Bunkus Date: Tue, 18 Sep 2018 22:07:41 +0200 Subject: [PATCH] mkvmerge: implement adjusting chapter timestamps with --sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There's a new option called `--chapter-sync …` that behaves just like `--sync -2:…`. Both can be used to adjust the timestamps of chapters read from containers (such as Matroska or MP4 files) and chapter files (both the XML format and the Ogg comment style format). Part of the implementation of #2358. --- NEWS.md | 4 ++ src/common/chapters/chapters.cpp | 10 ++-- src/common/chapters/chapters.h | 2 +- src/input/r_matroska.cpp | 14 +++++- src/input/r_mpeg_ts.cpp | 8 ++++ src/input/r_ogm.cpp | 8 ++++ src/input/r_qtmp4.cpp | 8 ++++ src/merge/mkvmerge.cpp | 61 ++++++++++++++++++------ src/merge/track_info.h | 5 ++ tests/results.txt | 1 + tests/test-651sync_chapter_timestamps.rb | 28 +++++++++++ 11 files changed, 127 insertions(+), 22 deletions(-) create mode 100755 tests/test-651sync_chapter_timestamps.rb diff --git a/NEWS.md b/NEWS.md index cf24b5503..93553a65c 100644 --- a/NEWS.md +++ b/NEWS.md @@ -6,6 +6,10 @@ user is about to create a file that won't contain audio tracks. It does this by default if at least one source file contains an audio track. Implements #2380. +* mkvmerge: chapters: the timestamps of chapters read from containers or from + chapter files can be adjusted (multiplication and addition) with the new + `--chapter-sync` option or using the special track ID `-2` for the existing + `--sync` option. Part of the implementation of #2358. ## Bug fixes diff --git a/src/common/chapters/chapters.cpp b/src/common/chapters/chapters.cpp index dafa15e1e..3ca68fc63 100644 --- a/src/common/chapters/chapters.cpp +++ b/src/common/chapters/chapters.cpp @@ -962,7 +962,9 @@ move_by_edition(KaxChapters &dst, */ void adjust_timestamps(EbmlMaster &master, - int64_t offset) { + int64_t offset, + int64_t numerator, + int64_t denominator) { size_t master_idx; for (master_idx = 0; master.ListSize() > master_idx; master_idx++) { if (!Is(master[master_idx])) @@ -973,16 +975,16 @@ adjust_timestamps(EbmlMaster &master, auto end = FindChild(atom); if (start) - start->SetValue(std::max(static_cast(start->GetValue()) + offset, 0)); + start->SetValue(std::max(static_cast((start->GetValue()) * numerator / denominator) + offset, 0)); if (end) - end->SetValue(std::max(static_cast(end->GetValue()) + offset, 0)); + end->SetValue(std::max(static_cast((end->GetValue()) * numerator / denominator) + offset, 0)); } for (master_idx = 0; master.ListSize() > master_idx; master_idx++) { auto work_master = dynamic_cast(master[master_idx]); if (work_master) - adjust_timestamps(*work_master, offset); + adjust_timestamps(*work_master, offset, numerator, denominator); } } diff --git a/src/common/chapters/chapters.h b/src/common/chapters/chapters.h index 537b821f3..24f8e13aa 100644 --- a/src/common/chapters/chapters.h +++ b/src/common/chapters/chapters.h @@ -84,7 +84,7 @@ libmatroska::KaxEditionEntry *find_edition_with_uid(libmatroska::KaxChapters &ch libmatroska::KaxChapterAtom *find_chapter_with_uid(libmatroska::KaxChapters &chapters, uint64_t uid); void move_by_edition(libmatroska::KaxChapters &dst, libmatroska::KaxChapters &src); -void adjust_timestamps(libebml::EbmlMaster &master, int64_t offset); +void adjust_timestamps(libebml::EbmlMaster &master, int64_t offset, int64_t numerator = 1, int64_t denominator = 1); void merge_entries(libebml::EbmlMaster &master); int count_atoms(libebml::EbmlMaster &master); void regenerate_uids(libebml::EbmlMaster &master); diff --git a/src/input/r_matroska.cpp b/src/input/r_matroska.cpp index 833b7aa8d..49d200586 100644 --- a/src/input/r_matroska.cpp +++ b/src/input/r_matroska.cpp @@ -1447,8 +1447,15 @@ kax_reader_c::read_headers() { void kax_reader_c::adjust_chapter_timestamps() { - if (m_chapters && (0 != m_global_timestamp_offset)) - mtx::chapters::adjust_timestamps(*m_chapters, -m_global_timestamp_offset); + if (!m_chapters) + return; + + auto const &sync = mtx::includes(m_ti.m_timestamp_syncs, track_info_c::chapter_track_id) ? m_ti.m_timestamp_syncs[track_info_c::chapter_track_id] + : mtx::includes(m_ti.m_timestamp_syncs, track_info_c::all_tracks_id) ? m_ti.m_timestamp_syncs[track_info_c::all_tracks_id] + : timestamp_sync_t{}; + + mtx::chapters::adjust_timestamps(*m_chapters, -m_global_timestamp_offset); + mtx::chapters::adjust_timestamps(*m_chapters, sync.displacement, sync.numerator, sync.denominator); } void @@ -2787,6 +2794,9 @@ void kax_reader_c::add_available_track_ids() { for (auto &track : m_tracks) add_available_track_id(track->tnum); + + if (m_chapters) + add_available_track_id(track_info_c::chapter_track_id); } void diff --git a/src/input/r_mpeg_ts.cpp b/src/input/r_mpeg_ts.cpp index 22f63c0fd..af9fddb5d 100644 --- a/src/input/r_mpeg_ts.cpp +++ b/src/input/r_mpeg_ts.cpp @@ -1308,6 +1308,11 @@ reader_c::process_chapter_entries() { } mtx::chapters::align_uids(m_chapters.get()); + + auto const &sync = mtx::includes(m_ti.m_timestamp_syncs, track_info_c::chapter_track_id) ? m_ti.m_timestamp_syncs[track_info_c::chapter_track_id] + : mtx::includes(m_ti.m_timestamp_syncs, track_info_c::all_tracks_id) ? m_ti.m_timestamp_syncs[track_info_c::all_tracks_id] + : timestamp_sync_t{}; + mtx::chapters::adjust_timestamps(*m_chapters, sync.displacement, sync.numerator, sync.denominator); } reader_c::~reader_c() { @@ -2375,6 +2380,9 @@ reader_c::create_packetizers() { void reader_c::add_available_track_ids() { add_available_track_id_range(0, m_tracks.size() - 1); + + if (m_chapters) + add_available_track_id(track_info_c::chapter_track_id); } bool diff --git a/src/input/r_ogm.cpp b/src/input/r_ogm.cpp index bdc0091b2..c3f2a1824 100644 --- a/src/input/r_ogm.cpp +++ b/src/input/r_ogm.cpp @@ -867,6 +867,11 @@ ogm_reader_c::handle_stream_comments() { chapters_set = true; mtx::chapters::align_uids(m_chapters.get()); + + auto const &sync = mtx::includes(m_ti.m_timestamp_syncs, track_info_c::chapter_track_id) ? m_ti.m_timestamp_syncs[track_info_c::chapter_track_id] + : mtx::includes(m_ti.m_timestamp_syncs, track_info_c::all_tracks_id) ? m_ti.m_timestamp_syncs[track_info_c::all_tracks_id] + : timestamp_sync_t{}; + mtx::chapters::adjust_timestamps(*m_chapters, sync.displacement, sync.numerator, sync.denominator); } catch (...) { exception_parsing_chapters = true; } @@ -893,6 +898,9 @@ ogm_reader_c::handle_stream_comments() { void ogm_reader_c::add_available_track_ids() { add_available_track_id_range(sdemuxers.size()); + + if (m_chapters) + add_available_track_id(track_info_c::chapter_track_id); } // ----------------------------------------------------------- diff --git a/src/input/r_qtmp4.cpp b/src/input/r_qtmp4.cpp index 49bbd160c..f05c27ac3 100644 --- a/src/input/r_qtmp4.cpp +++ b/src/input/r_qtmp4.cpp @@ -1211,6 +1211,11 @@ qtmp4_reader_c::process_chapter_entries(int level, m_chapters = mtx::chapters::parse(&text_out, 0, -1, 0, m_ti.m_chapter_language, "", true); mtx::chapters::align_uids(m_chapters.get()); + auto const &sync = mtx::includes(m_ti.m_timestamp_syncs, track_info_c::chapter_track_id) ? m_ti.m_timestamp_syncs[track_info_c::chapter_track_id] + : mtx::includes(m_ti.m_timestamp_syncs, track_info_c::all_tracks_id) ? m_ti.m_timestamp_syncs[track_info_c::all_tracks_id] + : timestamp_sync_t{}; + mtx::chapters::adjust_timestamps(*m_chapters, sync.displacement, sync.numerator, sync.denominator); + } catch (mtx::chapters::parser_x &ex) { mxerror(boost::format(Y("The MP4 file '%1%' contains chapters whose format was not recognized. This is often the case if the chapters are not encoded in UTF-8. Use the '--chapter-charset' option in order to specify the charset to use.\n")) % m_ti.m_fname); } @@ -2039,6 +2044,9 @@ qtmp4_reader_c::add_available_track_ids() { for (i = 0; i < m_demuxers.size(); ++i) add_available_track_id(m_demuxers[i]->id); + + if (m_chapters) + add_available_track_id(track_info_c::chapter_track_id); } std::string diff --git a/src/merge/mkvmerge.cpp b/src/merge/mkvmerge.cpp index 340dfcc28..d3925b4f0 100644 --- a/src/merge/mkvmerge.cpp +++ b/src/merge/mkvmerge.cpp @@ -93,6 +93,13 @@ set_usage() { usage_text += Y(" --chapters Read chapter information from the file.\n"); usage_text += Y(" --chapter-language Set the 'language' element in chapter entries.\n"); usage_text += Y(" --chapter-charset Charset for a simple chapter file.\n"); + usage_text += Y(" --chapter-sync \n" + " Synchronize, adjust the chapters's timestamps\n" + " by 'd' ms.\n" + " 'o/p': Adjust the timestamps by multiplying with\n" + " 'o/p' to fix linear drifts. 'p' defaults to\n" + " 1 if omitted. Both 'o' and 'p' can be\n" + " floating point numbers.\n"); usage_text += Y(" --cue-chapter-name-format \n" " Pattern for the conversion from cue sheet\n" " entries to chapter names.\n"); @@ -563,24 +570,31 @@ parse_arg_tracks(std::string s, static void parse_arg_sync(std::string s, std::string const &opt, - track_info_c &ti) { + track_info_c &ti, + boost::optional force_track_id) { timestamp_sync_t tcsync; // Extract the track number. - std::string orig = s; - std::vector parts = split(s, ":", 2); - if (parts.size() != 2) - mxerror(boost::format(Y("Invalid sync option. No track ID specified in '%1% %2%'.\n")) % opt % s); - + auto orig = s; int64_t id = 0; - if (!parse_number(parts[0], id)) - mxerror(boost::format(Y("Invalid track ID specified in '%1% %2%'.\n")) % opt % s); - s = parts[1]; - if (s.size() == 0) - mxerror(boost::format(Y("Invalid sync option specified in '%1% %2%'.\n")) % opt % orig); + if (!force_track_id) { + auto parts = split(s, ":", 2); + if (parts.size() != 2) + mxerror(boost::format(Y("Invalid sync option. No track ID specified in '%1% %2%'.\n")) % opt % s); - if (parts[1] == "reset") { + if (!parse_number(parts[0], id)) + mxerror(boost::format(Y("Invalid track ID specified in '%1% %2%'.\n")) % opt % s); + + s = parts[1]; + + if (s.size() == 0) + mxerror(boost::format(Y("Invalid sync option specified in '%1% %2%'.\n")) % opt % orig); + + } else + id = *force_track_id; + + if (s == "reset") { ti.m_reset_timestamps_specs[id] = true; return; } @@ -1860,7 +1874,8 @@ parse_arg_chapter_charset(const std::string &arg, static void parse_arg_chapters(const std::string ¶m, - const std::string &arg) { + const std::string &arg, + track_info_c &ti) { if (g_chapter_file_name != "") mxerror(boost::format(Y("Only one chapter file allowed in '%1% %2%'.\n")) % param % arg); @@ -1868,6 +1883,15 @@ parse_arg_chapters(const std::string ¶m, g_chapter_file_name = arg; g_kax_chapters = mtx::chapters::parse(g_chapter_file_name, 0, -1, 0, g_chapter_language.c_str(), g_chapter_charset.c_str(), false, &format, &g_tags_from_cue_chapters); + auto sync = ti.m_timestamp_syncs.find(track_info_c::chapter_track_id); + if (sync == ti.m_timestamp_syncs.end()) + sync = ti.m_timestamp_syncs.find(track_info_c::all_tracks_id); + + if (sync != ti.m_timestamp_syncs.end()) { + mtx::chapters::adjust_timestamps(*g_kax_chapters, sync->second.displacement, sync->second.numerator, sync->second.denominator); + ti.m_timestamp_syncs.erase(sync); + } + if (g_segment_title_set || !g_tags_from_cue_chapters || (mtx::chapters::format_e::cue != format)) return; @@ -2450,6 +2474,13 @@ parse_args(std::vector args) { parse_arg_chapter_charset(next_arg, *ti); sit++; + } else if (this_arg == "--chapter-sync") { + if (no_next_arg) + mxerror(boost::format(Y("'%1%' lacks the delay.\n")) % this_arg); + + parse_arg_sync(next_arg, this_arg, *ti, track_info_c::chapter_track_id); + sit++; + } else if (this_arg == "--cue-chapter-name-format") { if (no_next_arg) mxerror(Y("'--cue-chapter-name-format' lacks the format.\n")); @@ -2464,7 +2495,7 @@ parse_args(std::vector args) { if (no_next_arg) mxerror(Y("'--chapters' lacks the file name.\n")); - parse_arg_chapters(this_arg, next_arg); + parse_arg_chapters(this_arg, next_arg, *ti); sit++; inputs_found = true; @@ -2757,7 +2788,7 @@ parse_args(std::vector args) { if (no_next_arg) mxerror(boost::format(Y("'%1%' lacks the delay.\n")) % this_arg); - parse_arg_sync(next_arg, this_arg, *ti); + parse_arg_sync(next_arg, this_arg, *ti, boost::none); sit++; } else if (this_arg == "--cues") { diff --git a/src/merge/track_info.h b/src/merge/track_info.h index 9840878f8..21fad5e26 100644 --- a/src/merge/track_info.h +++ b/src/merge/track_info.h @@ -228,6 +228,11 @@ protected: bool m_initialized; public: + enum special_track_id_e { + all_tracks_id = -1, + chapter_track_id = -2, + }; + // The track ID. int64_t m_id; diff --git a/tests/results.txt b/tests/results.txt index 6f99cd049..2f0e03945 100644 --- a/tests/results.txt +++ b/tests/results.txt @@ -496,3 +496,4 @@ T_647recode_textsubs_from_matroska:2e63dc90381d8f5191b852aac6cc3b05-b297cba0182c T_648append_matroska_first_timestamp_not_zero:80d6193277012fde546499d832b4bab3:passed:20180724-210331:0.023501574 T_649unsupported_file_types:ok-ok-ok-ok-ok:passed:20180806-201225:0.045725267 T_650chapter_generation_no_names:65b6db53e376326e7e2f3d1c04caf63d-ok:passed:20180821-140249:0.030599141 +T_651sync_chapter_timestamps:840ec2d37a993e2da78c51b05d7c80f0-96a2f405d04267a1d1e62da4d9521a10-809ab88c5f5d48cfaf5f9bcf94b310f5-809ab88c5f5d48cfaf5f9bcf94b310f5-e0869563706cd2a0b3ec8cb65d1d0cd0-6890c5bac2022d3624958b950d36c77e-6890c5bac2022d3624958b950d36c77e-3e619d7ec77e8395eda20d91382df5cc-822141ff77af0c4c4c7a889ec7374190-3cc5d932c804e0bb1808e63501ea3d1e-3cc5d932c804e0bb1808e63501ea3d1e-7b65c3ff93bf15ca36889d4d9cfda9c1-9a6595a1dbab62f1174c4056e0fdba5b-9a6595a1dbab62f1174c4056e0fdba5b-0621718db4ce9cb7ed4c8c5dc843a962-b276bee9b55fc8aa874608bc2863ba46-00f74b92df721cd1dc363deddc4cf6d8-00f74b92df721cd1dc363deddc4cf6d8-f5d087300a71d86b1ca57ab7929758ed-df9f1da488db25294f457e3576ed8a2d-df9f1da488db25294f457e3576ed8a2d-a5e0963d2fa4ed1dc6e4a57e81c6ef75-7f60d2a169cd162d1de5e5df2a191a8a-7f60d2a169cd162d1de5e5df2a191a8a-7f60d2a169cd162d1de5e5df2a191a8a-91cd9376b3245c3e23413889f66333a1-91cd9376b3245c3e23413889f66333a1-91cd9376b3245c3e23413889f66333a1:passed:20180918-223701:2.330201791 diff --git a/tests/test-651sync_chapter_timestamps.rb b/tests/test-651sync_chapter_timestamps.rb new file mode 100755 index 000000000..136de450b --- /dev/null +++ b/tests/test-651sync_chapter_timestamps.rb @@ -0,0 +1,28 @@ +#!/usr/bin/ruby -w + +# T_651sync_chapter_timestamps +describe "mkvmerge / syncing chapter timestamps" + +sources = [ + [ "data/ogg/with_chapters.ogm", "--chapter-charset ISO-8859-15" ], + "data/mp4/o12-short.m4v", + "data/mkv/chapters-with-ebmlvoid.mkv", +] + +sources.each do |source| + source = [ source, "" ] unless source.is_a? Array + + test_merge source[0], :args => "#{source[1]}" + + [ "", "-" ].each do |sign| + [ -1, -2 ].each { |id| test_merge source[0], :args => "#{source[1]} --sync #{id}:#{sign}1000,3/2" } + test_merge source[0], :args => "#{source[1]} --chapter-sync #{sign}1000,3/2" + end +end + +[ "", + "--chapter-sync 1000,3/2", "--sync -1:1000,3/2", "--sync -2:1000,3/2", + "--chapter-sync -1000,3/2", "--sync -1:-1000,3/2", "--sync -2:-1000,3/2", +].each do |args| + test_merge "data/subtitles/srt/ven.srt", :args => "#{args} --chapters data/chapters/uk-and-gb.xml" +end