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