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();