diff --git a/ChangeLog b/ChangeLog index 4f6f2ab2d..2cfaf3a64 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,7 @@ +2003-11-10 Moritz Bunkus + + * mkvmerge: new feature: CUE sheets can be used for chapters. + 2003-11-09 Moritz Bunkus * mkvmerge: Added support for --sync for VobSub tracks. diff --git a/doc/mkvmerge.1 b/doc/mkvmerge.1 index bb9153972..0f042d13f 100644 --- a/doc/mkvmerge.1 +++ b/doc/mkvmerge.1 @@ -38,7 +38,7 @@ Sets the general title for the output file, e.g. the movie name. Read global tags from the XML \fIfile\fR. See the section about tags below for details. .TP -\fB\-\-command\-line\-charset\fR <\fBcharset\fR> +\fB\-\-command\-line\-charset\fR <\fIcharset\fR> Sets the charset to convert strings given on the command line from. It defaults to the charset given by system's current locale. This settings applies to arguments of the following options: \fB\-\-title\fR, \fB\-\-track\-name\fR and @@ -47,16 +47,35 @@ arguments of the following options: \fB\-\-title\fR, \fB\-\-track\-name\fR and .LP Chapter handling: (global options) .TP -\fB\-\-chapter\-language\fR <\fBlanguage\fR> +\fB\-\-chapter\-language\fR <\fIlanguage\fR> Sets the ISO639-2 language code that is written for each chapter entry. Applies only to simple chapter files. Defaults to "eng". See the section about chapters below for details. .TP -\fB\-\-chapter\-charset\fR <\fBcharset\fR> +\fB\-\-chapter\-charset\fR <\fIcharset\fR> Sets the charset that is used for the conversion to UTF-8 for simple chapter files. Defaults to the current system locale. See the section about chapters below for details. .TP +\fB\-\-cue\-chapter\-name\-format\fR <\fIformat\fR> +\fBmkvmerge\fR supports reading CUE sheets for audio files as the input for +chapters. CUE sheets usually contain the entries \fIPERFORMER\fR and +\fITITLE\fR for each index entry. \fBmkvmerge\fR uses these two strings +in order to construct the chapter name. With this option the format used +for this name can be set. The following meta characters are supported: +.br +\fB%p\fR is replaced by the current entry's \fIPERFORMER\fR string, +.br +\fB%t\fR is replaced by the current entry's \fITITLE\fR string and +.br +\fB%n\fR is replaced by the current track number. +.br +Everything else is copied as-is. +.br +If this option is not given then \fBmkvmerge\fR defaults to the +format '\fI%p - %t\fR' (the performer, followed by a space, a dash, +another space and the title). +.TP \fB\-\-chapters <\fIfile\fR> Read chapter information from the \fIfile\fR. See the section about chapters below for details. diff --git a/src/common/chapters.cpp b/src/common/chapters.cpp index dd0112bfd..c402930d3 100644 --- a/src/common/chapters.cpp +++ b/src/common/chapters.cpp @@ -27,6 +27,7 @@ #include "chapters.h" #include "error.h" +#include "mkvmerge.h" using namespace std; using namespace libmatroska; @@ -231,6 +232,245 @@ KaxChapters *parse_simple_chapters(mm_text_io_c *in, int64_t min_tc, // }}} +// PERFORMER "Blackmore's Night" +// TITLE "Fires At Midnight" +// FILE "Range.wav" WAVE +// TRACK 01 AUDIO +// TITLE "Written In The Stars" +// PERFORMER "Blackmore's Night" +// INDEX 01 00:00:00 +// TRACK 02 AUDIO +// TITLE "The Times They Are A Changin'" +// PERFORMER "Blackmore's Night" +// INDEX 00 04:46:62 +// INDEX 01 04:49:64 + +bool probe_cue_chapters(mm_text_io_c *in) { + string s; + + in->setFilePointer(0); + if (!in->getline2(s)) + return false; + if (starts_with_case(s, "performer ") || starts_with_case(s, "title ") || + starts_with_case(s, "file ")) + return true; + return false; +} + +char *cue_to_chapter_name_format = NULL; +static void cue_entries_to_chapter_name(string &performer, string &title, + string &global_performer, + string &global_title, string &name, + int num) { + const char *this_char, *next_char; + + name = ""; + if (title.length() == 0) + title = global_title; + if (performer.length() == 0) + performer = global_performer; + + if (cue_to_chapter_name_format == NULL) + this_char = "%p - %t"; + else + this_char = cue_to_chapter_name_format; + next_char = this_char + 1; + while (*this_char != 0) { + if (*this_char == '%') { + if (*next_char == 'p') + name += performer; + else if (*next_char == 't') + name += title; + else if (*next_char == 'n') + name += to_string(num); + else { + name += *this_char; + this_char--; + } + this_char++; + } else + name += *this_char; + this_char++; + next_char = this_char + 1; + } +} + +KaxChapters *parse_cue_chapters(mm_text_io_c *in, int64_t min_tc, + int64_t max_tc, int64_t offset, + const char *language, const char *charset, + bool exception_on_error) { + KaxChapters *chapters; + KaxEditionEntry *edition; + KaxChapterAtom *atom; + KaxChapterDisplay *display; + int line_num, num, index, min, sec, csec, cc_utf8; + int64_t start; + string global_title, global_performer, title, performer, line, name; + UTFstring wchar_string; + bool do_convert; + char *recoded_string; + + in->setFilePointer(0); + chapters = new KaxChapters; + + if (in->get_byte_order() == BO_NONE) { + do_convert = true; + cc_utf8 = utf8_init(charset); + + } else + do_convert = false; + + if (language == NULL) + language = "eng"; + + atom = NULL; + edition = NULL; + num = 0; + line_num = 0; + start = -1; + try { + while (in->getline2(line)) { + line_num++; + strip(line); + if ((line.length() == 0) || starts_with_case(line, "file ")) + continue; + + if (starts_with_case(line, "performer ")) { + line.erase(0, 10); + strip(line); + if (line.length() < 3) + continue; + if (line[0] == '"') + line.erase(0, 1); + if (line[line.length() - 1] == '"') + line.erase(line.length() - 1); + if (num == 0) + global_performer = line; + else + performer = line; + + } else if (starts_with_case(line, "title ")) { + line.erase(0, 6); + strip(line); + if (line.length() < 3) + continue; + if (line[0] == '"') + line.erase(0, 1); + if (line[line.length() - 1] == '"') + line.erase(line.length() - 1); + if (num == 0) + global_title = line; + else + title = line; + + } else if (starts_with_case(line, "index ")) { + line.erase(0, 6); + strip(line); + if (sscanf(line.c_str(), "%d %d:%d:%d", &index, &min, &sec, &csec) < 4) + mxerror("Cue sheet parser: Invalid INDEX entry in line %d.\n", + line_num); + if ((start == -1) || (index == 1)) + start = (min * 60 * 100 + sec * 100 + csec) * 10; + + } else if (starts_with_case(line, "track ")) { + if ((line.length() < 5) || strcasecmp(&line[line.length() - 5], + "audio")) + continue; + if (num >= 1) { + if (start == -1) + mxerror("Cue sheet parser: No INDEX entry found for the previous " + "TRACK entry (current line: %d)\n", line_num); + if (!((start >= min_tc) && ((start <= max_tc) || (max_tc == -1)))) + continue; + + if (edition == NULL) + edition = &GetChild(*chapters); + if (atom == NULL) + atom = &GetChild(*edition); + else + atom = &GetNextChild(*edition, *atom); + + *static_cast(&GetChild(*atom)) = + create_unique_uint32(); + + *static_cast(&GetChild(*atom)) = + (start - offset) * 1000000; + + display = &GetChild(*atom); + + cue_entries_to_chapter_name(performer, title, global_performer, + global_title, name, num); + if (do_convert) { + recoded_string = to_utf8(cc_utf8, name.c_str()); + wchar_string = cstrutf8_to_UTFstring(recoded_string); + safefree(recoded_string); + } else + wchar_string = cstrutf8_to_UTFstring(name.c_str()); + *static_cast + (&GetChild(*display)) = wchar_string; + + *static_cast(&GetChild(*display)) = + language; + } + num++; + + start = -1; + performer = ""; + title = ""; + } + } + + if (num >= 1) { + if (start == -1) + mxerror("Cue sheet parser: No INDEX entry found for the previous " + "TRACK entry (current line: %d)\n", line_num); + if ((start >= min_tc) && ((start <= max_tc) || (max_tc == -1))) { + if (edition == NULL) + edition = &GetChild(*chapters); + if (atom == NULL) + atom = &GetChild(*edition); + else + atom = &GetNextChild(*edition, *atom); + + *static_cast(&GetChild(*atom)) = + create_unique_uint32(); + + *static_cast(&GetChild(*atom)) = + (start - offset) * 1000000; + + display = &GetChild(*atom); + + cue_entries_to_chapter_name(performer, title, global_performer, + global_title, name, num); + if (do_convert) { + recoded_string = to_utf8(cc_utf8, name.c_str()); + wchar_string = cstrutf8_to_UTFstring(recoded_string); + safefree(recoded_string); + } else + wchar_string = cstrutf8_to_UTFstring(name.c_str()); + *static_cast + (&GetChild(*display)) = wchar_string; + + *static_cast(&GetChild(*display)) = + language; + } + } + } catch(error_c e) { + delete in; + delete chapters; + throw error_c(e); + } + + delete in; + + if (num == 0) { + delete chapters; + return NULL; + } + + return chapters; +} + KaxChapters *parse_chapters(const char *file_name, int64_t min_tc, int64_t max_tc, int64_t offset, const char *language, const char *charset, @@ -253,6 +493,11 @@ KaxChapters *parse_chapters(const char *file_name, int64_t min_tc, *is_simple_format = true; return parse_simple_chapters(in, min_tc, max_tc, offset, language, charset, exception_on_error); + } else if (probe_cue_chapters(in)) { + if (is_simple_format != NULL) + *is_simple_format = true; + return parse_cue_chapters(in, min_tc, max_tc, offset, language, + charset, exception_on_error); } else if (is_simple_format != NULL) *is_simple_format = false; diff --git a/src/common/chapters.h b/src/common/chapters.h index a462380da..ac10e62b1 100644 --- a/src/common/chapters.h +++ b/src/common/chapters.h @@ -49,6 +49,13 @@ KaxChapters *parse_simple_chapters(mm_text_io_c *in, int64_t min_tc, const char *charset, bool exception_on_error = false); +extern char *cue_to_chapter_name_format; +bool probe_cue_chapters(mm_text_io_c *in); +KaxChapters *parse_cue_chapters(mm_text_io_c *in, int64_t min_tc, + int64_t max_tc, int64_t offset, + const char *language, + const char *charset, + bool exception_on_error = false); void write_chapters_xml(KaxChapters *chapters, FILE *out); void write_chapters_simple(int &chapter_num, KaxChapters *chapters, FILE *out); diff --git a/src/mkvmerge.cpp b/src/mkvmerge.cpp index 3825f0185..178cc0255 100644 --- a/src/mkvmerge.cpp +++ b/src/mkvmerge.cpp @@ -256,6 +256,9 @@ static void usage() { " --chapters Read chapter information from the file.\n" " --chapter-language Set the 'language' element in chapter entries." "\n --chapter-charset Charset for a simple chapter file.\n" + " --cue-chapter-name-format\n" + " Pattern for the conversion from CUE sheet\n" + " entries to chapter names.\n" "\n General output control (advanced global options):\n" " --cluster-length Put at most n data blocks into each cluster.\n" " If the number is postfixed with 'ms' then\n" @@ -1430,6 +1433,17 @@ static void parse_args(int argc, char **argv) { chapter_charset = safestrdup(next_arg); i++; + } else if (!strcmp(this_arg, "--cue-chapter-name-format")) { + if (next_arg == NULL) + mxerror("'--cue-chapter-name-format' lacks the format.\n"); + if (chapter_file_name != NULL) + mxerror("'--cue-chapter-name-format' must be given before '--chapters'" + ".\n"); + + safefree(cue_to_chapter_name_format); + cue_to_chapter_name_format = safestrdup(next_arg); + i++; + } else if (!strcmp(this_arg, "--chapters")) { if (next_arg == NULL) mxerror("'--chapters' lacks the file name.\n"); @@ -1854,6 +1868,11 @@ static void cleanup() { if (kax_as != NULL) delete kax_as; + safefree(chapter_file_name); + safefree(chapter_language); + safefree(chapter_charset); + safefree(cue_to_chapter_name_format); + utf8_done(); }