From ffa5a2021d4011fdb3e854adcbd21591f8460e62 Mon Sep 17 00:00:00 2001 From: Karl Blomster Date: Wed, 13 May 2009 20:24:21 +0000 Subject: [PATCH] - Reworked the SMPTE timecode handling with Plorkyeran's help. It does now handle dropframe timecodes as well; the ms->SMPTE handling has been tested and seems reasonably correct, while the reverse conversion remains untested and unused. The Adobe Encore export filter will now use dropframe timecodes properly (previously it would play pretend with wallclock hours/minutes/seconds and incorrect frame numbers). - Changed the SubtitleFormat::AskForFPS dialog box; removed the "PAL/NTSC only" choice and added a "show SMPTE dropframe" parameter instead. Also added 50fps as a choice. - While I was at it, reworked the TranStation export filter so it actually looks ahead to see if the next line will overlap with the current, and if so, move the end time of the current line backwards one frame, which fixes #767 Originally committed to SVN as r2920. --- aegisub/src/ass_time.cpp | 129 ++++++++++++++++---- aegisub/src/ass_time.h | 17 ++- aegisub/src/subtitle_format.cpp | 90 +++++++++----- aegisub/src/subtitle_format.h | 8 +- aegisub/src/subtitle_format_encore.cpp | 10 +- aegisub/src/subtitle_format_microdvd.cpp | 14 ++- aegisub/src/subtitle_format_transtation.cpp | 94 ++++++++------ aegisub/src/subtitle_format_transtation.h | 3 + 8 files changed, 255 insertions(+), 110 deletions(-) diff --git a/aegisub/src/ass_time.cpp b/aegisub/src/ass_time.cpp index 17905c426..e7ef8ade7 100644 --- a/aegisub/src/ass_time.cpp +++ b/aegisub/src/ass_time.cpp @@ -39,6 +39,7 @@ #include "config.h" #include +#include #include #include #include "ass_time.h" @@ -288,9 +289,18 @@ int AssTime::GetTimeCentiseconds() { return (time % 1000)/10; } -FractionalTime::FractionalTime (wxString separator, double numerator, double denominator) { - num = numerator; - den = denominator; +/////// +// Constructor +FractionalTime::FractionalTime (wxString separator, int numerator, int denominator, bool dropframe) { + drop = dropframe; + if (drop) { + // no dropframe for any other framerates + num = 30000; + den = 1001; + } else { + num = numerator; + den = denominator; + } sep = separator; // fractions < 1 are not welcome here @@ -300,20 +310,23 @@ FractionalTime::FractionalTime (wxString separator, double numerator, double den throw _T("FractionalTime: no separator specified"); } +/////// +// Destructor FractionalTime::~FractionalTime () { sep.Clear(); } -int64_t FractionalTime::ToMillisecs (wxString _text) { +/////// +// SMPTE text string to milliseconds conversion +int FractionalTime::ToMillisecs (wxString _text) { wxString text = _text; wxString re_str = _T(""); - wxString sep_e = _T("\\") + sep; // escape this just in case it may be a reserved regex character text.Trim(false); text.Trim(true); long h=0,m=0,s=0,ms=0,f=0; - // hour minute second fraction - re_str << _T("(\\d+)") << sep_e << _T("(\\d+)") << sep_e << _T("(\\d+)") << sep_e << _T("(\\d+)"); + // hour minute second fraction + re_str << _T("(\\d+)") << sep << _T("(\\d+)") << sep << _T("(\\d+)") << sep << _T("(\\d+)"); wxRegEx re(re_str, wxRE_ADVANCED); if (!re.IsValid()) @@ -321,32 +334,106 @@ int64_t FractionalTime::ToMillisecs (wxString _text) { if (!re.Matches(text)) return 0; // FIXME: throw here too? - re.GetMatch(text, 1).ToLong(&h); - re.GetMatch(text, 2).ToLong(&m); - re.GetMatch(text, 3).ToLong(&s); - re.GetMatch(text, 4).ToLong(&f); - // FIXME: find out how to do this in a sane way - //if ((double)f >= ((double)num/(double)den) // overflow? - // f = (num/den - 1); - ms = long((1000.0 / (num/den)) * (double)f); + re.GetMatch(text,1).ToLong(&h); + re.GetMatch(text,2).ToLong(&m); + re.GetMatch(text,3).ToLong(&s); + re.GetMatch(text,4).ToLong(&f); - return (int64_t)((h * 3600000) + (m * 60000) + (s * 1000) + ms); + int msecs_f = 0; + int fn = 0; + // dropframe? do silly things + if (drop) { + fn += h * frames_per_period * 6; + fn += (m % 10) * frames_per_period; + + if (m > 0) { + fn += 1800; + m--; + + fn += m * 1798; // two timestamps dropped per minute after the first + fn += s * 30 + f - 2; + } + else { // minute is evenly divisible by 10, keep first two timestamps + fn += s * 30; + fn += f; + } + + msecs_f = (fn * num) / den; + } + // no dropframe, may or may not sync with wallclock time + // (see comment in FromMillisecs for an explanation of why it's done like this) + else { + int fps_approx = floor((double(num)/double(den))+0.5); + fn += h * 3600 * fps_approx; + fn += m * 60 * fps_approx; + fn += s * fps_approx; + fn += f; + + msecs_f = (fn * num) / den; + } + + return msecs_f; } +/////// +// SMPTE text string to AssTime conversion AssTime FractionalTime::ToAssTime (wxString _text) { AssTime time; time.SetMS((int)ToMillisecs(_text)); return time; } +/////// +// AssTime to SMPTE text string conversion wxString FractionalTime::FromAssTime(AssTime time) { - return FromMillisecs((int64_t)time.GetMS()); + return FromMillisecs(time.GetMS()); } +/////// +// Milliseconds to SMPTE text string conversion wxString FractionalTime::FromMillisecs(int64_t msec) { - int h = msec / 3600000; - int m = (msec % 3600000)/60000; - int s = (msec % 60000)/1000; - int f = int((msec % 1000) * ((num/den) / 1000.0)); + int h=0, m=0, s=0, f=0; // hours, minutes, seconds, fractions + int fn = (msec*(int64_t)num) / (1000*den); // frame number + + // dropframe? + if (drop) { + fn += 2 * (fn / (30 * 60)) - 2 * (fn / (30 * 60 * 10)); + h = fn / (30 * 60 * 60); + m = (fn / (30 * 60)) % 60; + s = (fn / 30) % 60; + f = fn % 30; + } + // no dropframe; h/m/s may or may not sync to wallclock time + else { + /* + This is truly the dumbest shit. What we're trying to ensure here + is that non-integer framerates are desynced from the wallclock + time by a correct amount of time. For example, in the + NTSC-without-dropframe case, 3600*num/den would be 107892 + (when truncated to int), which is quite a good approximation of + how a long an hour is when counted in 30000/1001 frames per second. + Unfortunately, that's not what we want, since frame numbers will + still range from 00 to 29, meaning that we're really getting _30_ + frames per second and not 29.97 and the full hour will be off by + almost 4 seconds (108000 frames versus 107892). + + DEATH TO SMPTE + */ + int fps_approx = floor((double(num)/double(den))+0.5); + int frames_per_h = 3600*fps_approx; + int frames_per_m = 60*fps_approx; + int frames_per_s = fps_approx; + while (fn >= frames_per_h) { + h++; fn -= frames_per_h; + } + while (fn >= frames_per_m) { + m++; fn -= frames_per_m; + } + while (fn >= frames_per_s) { + s++; fn -= frames_per_s; + } + f = fn; + } + return wxString::Format(_T("%02i") + sep + _T("%02i") + sep + _T("%02i") + sep + _T("%02i"),h,m,s,f); } diff --git a/aegisub/src/ass_time.h b/aegisub/src/ass_time.h index 7e39d7baf..d112f2e47 100644 --- a/aegisub/src/ass_time.h +++ b/aegisub/src/ass_time.h @@ -78,21 +78,28 @@ bool operator <= (AssTime &t1, AssTime &t2); bool operator >= (AssTime &t1, AssTime &t2); + ///////////////////////////// // Class for that annoying SMPTE format timecodes stuff class FractionalTime { private: - int64_t time; // milliseconds, like in AssTime - double num, den; // numerator/denominator - wxString sep; // separator; someone might have separators of more than one character :V + int time; // milliseconds, like in AssTime + int num, den; // numerator/denominator + bool drop; // EVIL + wxString sep; // separator; someone might have separators of more than one character :V + + // A period is roughly 10 minutes and is used for the dropframe stuff; + // SMPTE dropframe timecodes drops 18 timestamps per 18000, hence the number 17982. + static const int frames_per_period = 17982; public: // dumb assumption? I give no fuck - FractionalTime(wxString separator, double numerator=30.0, double denominator=1.0); + // NOTE: separator can be a regex! at least if you only plan on doing SMPTE->somethingelse. + FractionalTime(wxString separator, int numerator=30, int denominator=1, bool dropframe=false); ~FractionalTime(); AssTime ToAssTime(wxString fractime); - int64_t ToMillisecs(wxString fractime); + int ToMillisecs(wxString fractime); wxString FromAssTime(AssTime time); wxString FromMillisecs(int64_t msec); diff --git a/aegisub/src/subtitle_format.cpp b/aegisub/src/subtitle_format.cpp index 43c7e0183..e4d483c7a 100644 --- a/aegisub/src/subtitle_format.cpp +++ b/aegisub/src/subtitle_format.cpp @@ -279,11 +279,13 @@ wxString SubtitleFormat::GetWildcards(int mode) { ///////////////////////////////// // Ask the user to enter the FPS -double SubtitleFormat::AskForFPS(bool palNtscOnly) { +SubtitleFormat::FPSRational SubtitleFormat::AskForFPS(bool showSMPTE) { wxArrayString choices; + FPSRational fps_rat; + fps_rat.smpte_dropframe = false; // ensure it's false by default // Video FPS - bool vidLoaded = !palNtscOnly && VFR_Output.IsLoaded(); + bool vidLoaded = VFR_Output.IsLoaded(); if (vidLoaded) { wxString vidFPS; if (VFR_Output.GetFrameRateType() == VFR) vidFPS = _T("VFR"); @@ -292,49 +294,73 @@ double SubtitleFormat::AskForFPS(bool palNtscOnly) { } // Standard FPS values - if (!palNtscOnly) { - choices.Add(_("15.000 FPS")); - choices.Add(_("23.976 FPS (Decimated NTSC)")); - choices.Add(_("24.000 FPS (FILM)")); - } + choices.Add(_("15.000 FPS")); + choices.Add(_("23.976 FPS (Decimated NTSC)")); + choices.Add(_("24.000 FPS (FILM)")); choices.Add(_("25.000 FPS (PAL)")); choices.Add(_("29.970 FPS (NTSC)")); - if (!palNtscOnly) { - choices.Add(_("30.000 FPS")); - choices.Add(_("59.940 FPS (NTSC x2)")); - choices.Add(_("60.000 FPS")); - choices.Add(_("119.880 FPS (NTSC x4)")); - choices.Add(_("120.000 FPS")); - } + if (showSMPTE) + choices.Add(_("29.970 FPS (NTSC with SMPTE dropframe)")); + choices.Add(_("30.000 FPS")); + choices.Add(_("50.000 FPS (PAL x2)")); + choices.Add(_("59.940 FPS (NTSC x2)")); + choices.Add(_("60.000 FPS")); + choices.Add(_("119.880 FPS (NTSC x4)")); + choices.Add(_("120.000 FPS")); // Ask int choice = wxGetSingleChoiceIndex(_("Please choose the appropriate FPS for the subtitles:"),_("FPS"),choices); - if (choice == -1) return 0.0; + if (choice == -1) { + fps_rat.num = 0; + fps_rat.den = 0; - // PAL/NTSC choice - if (palNtscOnly) { - if (choice == 0) return 25.0; - else return 30.0 / 1.001; + return fps_rat; } // Get FPS from choice if (vidLoaded) choice--; - switch (choice) { - case -1: return -1.0; break; // VIDEO - case 0: return 15.0; break; - case 1: return 24.0 / 1.001; break; - case 2: return 24.0; break; - case 3: return 25.0; break; - case 4: return 30.0 / 1.001; break; - case 5: return 30.0; break; - case 6: return 60.0 / 1.001; break; - case 7: return 60.0; break; - case 8: return 120.0 / 1.001; break; - case 9: return 120.0; break; + // dropframe was displayed, that means all choices >4 are bumped up by 1 + if (showSMPTE) { + switch (choice) { + case -1: fps_rat.num = -1; fps_rat.den = 1; break; // VIDEO + case 0: fps_rat.num = 15; fps_rat.den = 1; break; + case 1: fps_rat.num = 24000; fps_rat.den = 1001; break; + case 2: fps_rat.num = 24; fps_rat.den = 1; break; + case 3: fps_rat.num = 25; fps_rat.den = 1; break; + case 4: fps_rat.num = 30000; fps_rat.den = 1001; break; + case 5: fps_rat.num = 30000; fps_rat.den = 1001; fps_rat.smpte_dropframe = true; break; + case 6: fps_rat.num = 30; fps_rat.den = 1; break; + case 7: fps_rat.num = 50; fps_rat.den = 1; break; + case 8: fps_rat.num = 60000; fps_rat.den = 1001; break; + case 9: fps_rat.num = 60; fps_rat.den = 1; break; + case 10: fps_rat.num = 120000; fps_rat.den = 1001; break; + case 11: fps_rat.num = 120; fps_rat.den = 1; break; + } + return fps_rat; + } else { + // dropframe wasn't displayed + switch (choice) { + case -1: fps_rat.num = -1; fps_rat.den = 1; break; // VIDEO + case 0: fps_rat.num = 15; fps_rat.den = 1; break; + case 1: fps_rat.num = 24000; fps_rat.den = 1001; break; + case 2: fps_rat.num = 24; fps_rat.den = 1; break; + case 3: fps_rat.num = 25; fps_rat.den = 1; break; + case 4: fps_rat.num = 30000; fps_rat.den = 1001; break; + case 5: fps_rat.num = 30; fps_rat.den = 1; break; + case 6: fps_rat.num = 50; fps_rat.den = 1; break; + case 7: fps_rat.num = 60000; fps_rat.den = 1001; break; + case 8: fps_rat.num = 60; fps_rat.den = 1; break; + case 9: fps_rat.num = 120000; fps_rat.den = 1001; break; + case 10: fps_rat.num = 120; fps_rat.den = 1; break; + } + return fps_rat; } // fubar - return 0.0; + fps_rat.num = 0; + fps_rat.den = 0; + + return fps_rat; } diff --git a/aegisub/src/subtitle_format.h b/aegisub/src/subtitle_format.h index d0eec2801..ea6d34417 100644 --- a/aegisub/src/subtitle_format.h +++ b/aegisub/src/subtitle_format.h @@ -65,6 +65,12 @@ private: static bool loaded; protected: + struct FPSRational { + int num; + int den; + bool smpte_dropframe; + }; + std::list *Line; void CreateCopy(); @@ -81,7 +87,7 @@ protected: void LoadDefault(bool defline=true); AssFile *GetAssFile() { return assFile; } int AddLine(wxString data,wxString group,int lasttime,int &version,wxString *outgroup=NULL); - double AskForFPS(bool palNtscOnly=false); + FPSRational AskForFPS(bool showSMPTE=false); virtual wxString GetName()=0; virtual wxArrayString GetReadWildcards(); diff --git a/aegisub/src/subtitle_format_encore.cpp b/aegisub/src/subtitle_format_encore.cpp index 6e97db695..b20775feb 100644 --- a/aegisub/src/subtitle_format_encore.cpp +++ b/aegisub/src/subtitle_format_encore.cpp @@ -70,8 +70,8 @@ bool EncoreSubtitleFormat::CanWriteFile(wxString filename) { // Write file void EncoreSubtitleFormat::WriteFile(wxString _filename,wxString encoding) { // Get FPS - double fps = AskForFPS(true); - if (fps <= 0.0) return; + FPSRational fps_rat = AskForFPS(true); + if (fps_rat.num <= 0 || fps_rat.den <= 0) return; // Open file TextFileWriter file(_filename,encoding); @@ -88,14 +88,14 @@ void EncoreSubtitleFormat::WriteFile(wxString _filename,wxString encoding) { using std::list; int i = 0; - // Encore wants ; instead of : if we're dealing with NTSC - FractionalTime fp(fps > 26.0 ? _T(";") : _T(":"), fps); + // Encore wants ; instead of : if we're dealing with NTSC dropframe stuff + FractionalTime ft(fps_rat.smpte_dropframe ? _T(";") : _T(":"), fps_rat.num, fps_rat.den, fps_rat.smpte_dropframe); for (list::iterator cur=Line->begin();cur!=Line->end();cur++) { AssDialogue *current = AssEntry::GetAsDialogue(*cur); if (current && !current->Comment) { // Time stamps - wxString timeStamps = wxString::Format(_T("%i "),++i) + fp.FromAssTime(current->Start) + _T(" ") + fp.FromAssTime(current->End); + wxString timeStamps = wxString::Format(_T("%i "),++i) + ft.FromAssTime(current->Start) + _T(" ") + ft.FromAssTime(current->End); // Write file.WriteLineToFile(timeStamps + current->Text); diff --git a/aegisub/src/subtitle_format_microdvd.cpp b/aegisub/src/subtitle_format_microdvd.cpp index a1d8ae209..d8577d3f0 100644 --- a/aegisub/src/subtitle_format_microdvd.cpp +++ b/aegisub/src/subtitle_format_microdvd.cpp @@ -110,6 +110,7 @@ void MicroDVDSubtitleFormat::ReadFile(wxString filename,wxString forceEncoding) // Loop bool isFirst = true; + FPSRational fps_rat; double fps = 0.0; while (file.HasMoreLines()) { wxString line = file.ReadLineFromFile(); @@ -133,9 +134,9 @@ void MicroDVDSubtitleFormat::ReadFile(wxString filename,wxString forceEncoding) // If it wasn't an fps line, ask the user for it if (fps <= 0.0) { - fps = AskForFPS(); - if (fps == 0.0) return; - else if (fps > 0.0) cfr.SetCFR(fps); + fps_rat = AskForFPS(); + if (fps_rat.num == 0) return; + else if (fps_rat.num > 0) cfr.SetCFR(double(fps_rat.num)/double(fps_rat.den)); else rate = &VFR_Output; } else { @@ -172,9 +173,10 @@ void MicroDVDSubtitleFormat::WriteFile(wxString filename,wxString encoding) { // Set FPS FrameRate cfr; FrameRate *rate = 𝔠 - double fps = AskForFPS(); - if (fps == 0.0) return; - else if (fps > 0.0) cfr.SetCFR(fps); + FPSRational fps_rat = AskForFPS(); + if (fps_rat.num == 0 || fps_rat.den == 0) return; + double fps = double(fps_rat.num) / double(fps_rat.den); + if (fps > 0.0) cfr.SetCFR(fps); else rate = &VFR_Output; // Convert file diff --git a/aegisub/src/subtitle_format_transtation.cpp b/aegisub/src/subtitle_format_transtation.cpp index 0fa9549a4..59576d43d 100644 --- a/aegisub/src/subtitle_format_transtation.cpp +++ b/aegisub/src/subtitle_format_transtation.cpp @@ -74,8 +74,8 @@ bool TranStationSubtitleFormat::CanWriteFile(wxString filename) { // Write file void TranStationSubtitleFormat::WriteFile(wxString _filename,wxString encoding) { // Get FPS - double fps = AskForFPS(true); - if (fps <= 0.0) return; + FPSRational fps_rat = AskForFPS(true); + if (fps_rat.num <= 0 || fps_rat.den <= 0) return; // Open file TextFileWriter file(_filename,encoding); @@ -89,50 +89,22 @@ void TranStationSubtitleFormat::WriteFile(wxString _filename,wxString encoding) // Write lines using std::list; + AssDialogue *current = NULL; + AssDialogue *next = NULL; for (list::iterator cur=Line->begin();cur!=Line->end();cur++) { - AssDialogue *current = AssEntry::GetAsDialogue(*cur); + if (next) + current = next; + next = AssEntry::GetAsDialogue(*cur); + if (current && !current->Comment) { - // Get line data - AssStyle *style = GetAssFile()->GetStyle(current->Style); - int valign = 0; - wxChar *halign = _T(" "); // default is centered - wxChar *type = _T("N"); // no special style - if (style) { - if (style->alignment >= 4) valign = 4; - if (style->alignment >= 7) valign = 9; - if (style->alignment == 1 || style->alignment == 4 || style->alignment == 7) halign = _T("L"); - if (style->alignment == 3 || style->alignment == 6 || style->alignment == 9) halign = _T("R"); - if (style->italic) type = _T("I"); - } - - // Hack: If an italics-tag (\i1) appears anywhere in the line, - // make it all italics - if (current->Text.Find(_T("\\i1")) != wxNOT_FOUND)type = _T("I"); - - // Write header - AssTime start = current->Start; - AssTime end = current->End; - // Subtract half a frame duration from end time, since it is inclusive - // and we otherwise run the risk of having two lines overlap in a - // frame, when they should run right into each other. - end.SetMS(end.GetMS() - (int)(500.0/fps)); - FractionalTime ft(_T(":"),fps); - wxString header = wxString::Format(_T("SUB[%i%s%s "),valign,halign,type) + ft.FromAssTime(start) + _T(">") + ft.FromAssTime(end) + _T("]"); - file.WriteLineToFile(header); - - // Process text - wxString lineEnd = _T("\r\n"); - current->StripTags(); - current->Text.Replace(_T("\\h"),_T(" "),true); - current->Text.Replace(_T("\\n"),lineEnd,true); - current->Text.Replace(_T("\\N"),lineEnd,true); - while (current->Text.Replace(lineEnd+lineEnd,lineEnd,true)); - // Write text - file.WriteLineToFile(current->Text); + file.WriteLineToFile(ConvertLine(current,&fps_rat,(next && !next->Comment) ? next->Start.GetMS() : -1)); file.WriteLineToFile(_T("")); } } + // flush last line + if (next && !next->Comment) + file.WriteLineToFile(ConvertLine(next,&fps_rat,-1)); // Every file must end with this line file.WriteLineToFile(_T("SUB[")); @@ -140,3 +112,45 @@ void TranStationSubtitleFormat::WriteFile(wxString _filename,wxString encoding) // Clean up ClearCopy(); } + +wxString TranStationSubtitleFormat::ConvertLine(AssDialogue *current, FPSRational *fps_rat, int nextl_start) { + // Get line data + AssStyle *style = GetAssFile()->GetStyle(current->Style); + int valign = 0; + wxChar *halign = _T(" "); // default is centered + wxChar *type = _T("N"); // no special style + if (style) { + if (style->alignment >= 4) valign = 4; + if (style->alignment >= 7) valign = 9; + if (style->alignment == 1 || style->alignment == 4 || style->alignment == 7) halign = _T("L"); + if (style->alignment == 3 || style->alignment == 6 || style->alignment == 9) halign = _T("R"); + if (style->italic) type = _T("I"); + } + + // Hack: If an italics-tag (\i1) appears anywhere in the line, + // make it all italics + if (current->Text.Find(_T("\\i1")) != wxNOT_FOUND)type = _T("I"); + + // Write header + AssTime start = current->Start; + AssTime end = current->End; + + // Subtract one frame if the end time of the current line is equal to the + // start of next one, since the end timestamp is inclusive and the lines + // would overlap if left as is. + if (nextl_start > 0 && end.GetMS() == nextl_start) + end.SetMS(end.GetMS() - ((1000*fps_rat->den)/fps_rat->num)); + + FractionalTime ft(_T(":"), fps_rat->num, fps_rat->den, fps_rat->smpte_dropframe); + wxString header = wxString::Format(_T("SUB[%i%s%s "),valign,halign,type) + ft.FromAssTime(start) + _T(">") + ft.FromAssTime(end) + _T("]\r\n"); + + // Process text + wxString lineEnd = _T("\r\n"); + current->StripTags(); + current->Text.Replace(_T("\\h"),_T(" "),true); + current->Text.Replace(_T("\\n"),lineEnd,true); + current->Text.Replace(_T("\\N"),lineEnd,true); + while (current->Text.Replace(lineEnd+lineEnd,lineEnd,true)); + + return header + current->Text; +} \ No newline at end of file diff --git a/aegisub/src/subtitle_format_transtation.h b/aegisub/src/subtitle_format_transtation.h index 9eccc04a5..0e334fb1d 100644 --- a/aegisub/src/subtitle_format_transtation.h +++ b/aegisub/src/subtitle_format_transtation.h @@ -45,6 +45,9 @@ ////////////////////// // TranStation writer class TranStationSubtitleFormat : public SubtitleFormat { +private: + wxString ConvertLine(AssDialogue *line, FPSRational *fps_rat, int nextl_start); + public: wxString GetName(); wxArrayString GetWriteWildcards();