diff --git a/aegisub/libaegisub/common/vfr.cpp b/aegisub/libaegisub/common/vfr.cpp index 813367976..662f57f33 100644 --- a/aegisub/libaegisub/common/vfr.cpp +++ b/aegisub/libaegisub/common/vfr.cpp @@ -181,16 +181,18 @@ Framerate::Framerate(double fps) : denominator(default_denominator) , numerator(int64_t(fps * denominator)) , last(0) +, drop(false) { if (fps < 0.) throw BadFPS("FPS must be greater than zero"); if (fps > 1000.) throw BadFPS("FPS must not be greater than 1000"); timecodes.push_back(0); } -Framerate::Framerate(int64_t numerator, int64_t denominator) +Framerate::Framerate(int64_t numerator, int64_t denominator, bool drop) : denominator(denominator) , numerator(numerator) , last(0) +, drop(drop && numerator % denominator != 0) { if (numerator <= 0 || denominator <= 0) throw BadFPS("Numerator and denominator must both be greater than zero"); @@ -208,6 +210,7 @@ void Framerate::SetFromTimecodes() { Framerate::Framerate(std::vector const& timecodes) : timecodes(timecodes) +, drop(false) { SetFromTimecodes(); } @@ -314,5 +317,71 @@ int Framerate::TimeAtFrame(int frame, Time type) const { return timecodes[frame]; } +void Framerate::SmpteAtFrame(int frame, int *h, int *m, int *s, int *f) const { + frame = std::max(frame, 0); + int ifps = (int)ceil(FPS()); + + if (drop && denominator == 1001 && numerator % 30000 == 0) { + // NTSC skips the first two frames of every minute except for multiples + // of ten. For multiples of NTSC, simply multiplying the number of + // frames skips seems like the most sensible option. + const int drop_factor = int(numerator / 30000); + const int one_minute = 60 * 30 * drop_factor - drop_factor * 2; + const int ten_minutes = 60 * 10 * 30 * drop_factor - drop_factor * 18; + const int ten_minute_groups = frame / ten_minutes; + const int last_ten_minutes = frame % ten_minutes; + + frame += ten_minute_groups * 18 * drop_factor; + frame += (last_ten_minutes - 2 * drop_factor) / one_minute * 2 * drop_factor; + } + + // Non-integral frame rates other than NTSC aren't supported by SMPTE + // timecodes, but the user has asked for it so just give something that + // resembles a valid timecode which is no more than half a frame off + // wallclock time + else if (drop && ifps != FPS()) { + frame = int(frame / FPS() * ifps + 0.5); + } + + *h = frame / (ifps * 60 * 60); + *m = (frame / (ifps * 60)) % 60; + *s = (frame / ifps) % 60; + *f = frame % ifps; +} + +void Framerate::SmpteAtTime(int ms, int *h, int *m, int *s, int *f) const { + SmpteAtFrame(FrameAtTime(ms), h, m, s, f); +} + +int Framerate::FrameAtSmpte(int h, int m, int s, int f) const { + int ifps = (int)ceil(FPS()); + + if (drop && denominator == 1001 && numerator % 30000 == 0) { + const int drop_factor = int(numerator / 30000); + const int one_minute = 60 * 30 * drop_factor - drop_factor * 2; + const int ten_minutes = 60 * 10 * 30 * drop_factor - drop_factor * 18; + + const int ten_m = m / 10; + m = m % 10; + + // The specified frame doesn't actually exist so skip forward to the + // next frame that does + if (m != 0 && s == 0 && f < 2 * drop_factor) + f = 2 * drop_factor; + + return h * ten_minutes * 6 + ten_m * ten_minutes + m * one_minute + s * ifps + f; + } + else if (drop && ifps != FPS()) { + int frame = (h * 60 * 60 + m * 60 + s) * ifps + f; + return int((double)frame / ifps * FPS() + 0.5); + } + + return (h * 60 * 60 + m * 60 + s) * ifps + f; +} + +int Framerate::TimeAtSmpte(int h, int m, int s, int f) const { + return TimeAtFrame(FrameAtSmpte(h, m, s, f)); +} + } } diff --git a/aegisub/libaegisub/include/libaegisub/vfr.h b/aegisub/libaegisub/include/libaegisub/vfr.h index bb2738eb9..be1031c7f 100644 --- a/aegisub/libaegisub/include/libaegisub/vfr.h +++ b/aegisub/libaegisub/include/libaegisub/vfr.h @@ -83,6 +83,9 @@ class Framerate { /// Start time in milliseconds of each frame std::vector timecodes; + /// Does this frame rate need drop frames and have them enabled? + bool drop; + /// Set FPS properties from the timecodes vector void SetFromTimecodes(); public: @@ -102,7 +105,8 @@ public: /// @brief CFR constructor with rational timebase /// @param numerator Timebase numerator /// @param denominator Timebase denominator - Framerate(int64_t numerator, int64_t denominator); + /// @param drop Enable drop frames if the FPS requires it + Framerate(int64_t numerator, int64_t denominator, bool drop=true); /// @brief VFR from frame times /// @param timecodes Vector of frame start times in milliseconds @@ -139,6 +143,54 @@ public: /// results for all frame numbers int TimeAtFrame(int frame, Time type = EXACT) const; + /// @brief Get the components of the SMPTE timecode for the given time + /// @param[out] h Hours component + /// @param[out] m Minutes component + /// @param[out] s Seconds component + /// @param[out] f Frames component + /// + /// For NTSC (30000/1001), this generates proper SMPTE timecodes with drop + /// frames handled. For multiples of NTSC, this multiplies the number of + /// dropped frames. For other non-integral frame rates, it drops frames in + /// an undefined manner which results in no more than half a second error + /// from wall clock time. + /// + /// For integral frame rates, no frame dropping occurs. + void SmpteAtTime(int ms, int *h, int *m, int *s, int *f) const; + + /// @brief Get the components of the SMPTE timecode for the given frame + /// @param[out] h Hours component + /// @param[out] m Minutes component + /// @param[out] s Seconds component + /// @param[out] f Frames component + /// + /// For NTSC (30000/1001), this generates proper SMPTE timecodes with drop + /// frames handled. For multiples of NTSC, this multiplies the number of + /// dropped frames. For other non-integral frame rates, it drops frames in + /// an undefined manner which results in no more than half a second error + /// from wall clock time. + /// + /// For integral frame rates, no frame dropping occurs. + void SmpteAtFrame(int frame, int *h, int *m, int *s, int *f) const; + + /// @brief Get the frame indicated by the SMPTE timecode components + /// @param h Hours component + /// @param m Minutes component + /// @param s Seconds component + /// @param f Frames component + /// @return Frame number + /// @see SmpteAtFrame + int FrameAtSmpte(int h, int m, int s, int f) const; + + /// @brief Get the time indicated by the SMPTE timecode components + /// @param h Hours component + /// @param m Minutes component + /// @param s Seconds component + /// @param f Frames component + /// @return Time in milliseconds + /// @see SmpteAtTime + int TimeAtSmpte(int h, int m, int s, int f) const; + /// @brief Save the current time codes to a file as v2 timecodes /// @param file File name /// @param length Minimum number of frames to output @@ -149,9 +201,17 @@ public: /// be otherwise sensible. void Save(std::string const& file, int length = -1) const; + /// Is this frame rate possibly variable? bool IsVFR() const {return timecodes.size() > 1; } - bool IsLoaded() const { return numerator > 0; }; + + /// Does this represent a valid frame rate? + bool IsLoaded() const { return numerator > 0; } + + /// Get average FPS of this frame rate double FPS() const { return double(numerator) / denominator; } + + /// Does this frame rate need drop frames for SMPTE timeish frame numbers? + bool NeedsDropFrames() const { return drop; } }; } // namespace vfr diff --git a/aegisub/src/ass_time.cpp b/aegisub/src/ass_time.cpp index c108929e3..6a2ddb864 100644 --- a/aegisub/src/ass_time.cpp +++ b/aegisub/src/ass_time.cpp @@ -41,6 +41,8 @@ #ifndef AGI_PRE #include #include + +#include #endif #include "utils.h" @@ -101,59 +103,25 @@ int AssTime::GetTimeSeconds() const { return (time % 60000) / 1000; } int AssTime::GetTimeMiliseconds() const { return (time % 1000); } int AssTime::GetTimeCentiseconds() const { return (time % 1000) / 10; } -FractionalTime::FractionalTime(agi::vfr::Framerate fps, bool dropframe) +SmpteFormatter::SmpteFormatter(agi::vfr::Framerate fps, char sep) : fps(fps) -, drop(dropframe) +, sep(sep) { } -wxString FractionalTime::ToSMPTE(AssTime time, char sep) { - int h=0, m=0, s=0, f=0; // hours, minutes, seconds, fractions - int fn = fps.FrameAtTime(time); - - // return 00:00:00:00 - if (time <= 0) { - } - // dropframe? - else 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(fps.FPS() + 0.5); - int frames_per_h = 3600*fps_approx; - int frames_per_m = 60*fps_approx; - int frames_per_s = fps_approx; - - h = fn / frames_per_h; - fn = fn % frames_per_h; - - m = fn / frames_per_m; - fn = fn % frames_per_m; - - s = fn / frames_per_s; - fn = fn % frames_per_s; - - f = fn; - } - - return wxString::Format("%02i%c%02%c%02i%c%02i", h, sep, m, sep, s, sep, f); +wxString SmpteFormatter::ToSMPTE(AssTime time) const { + int h=0, m=0, s=0, f=0; + fps.SmpteAtTime(time, &h, &m, &s, &f); + return wxString::Format("%02d%c%02d%c%02d%c%02d", h, sep, m, sep, s, sep, f); +} + +AssTime SmpteFormatter::FromSMPTE(wxString const& str) const { + long h, m, s, f; + wxArrayString toks = wxStringTokenize(str, sep); + if (toks.size() != 4) return 0; + toks[0].ToLong(&h); + toks[1].ToLong(&m); + toks[2].ToLong(&s); + toks[3].ToLong(&f); + return fps.TimeAtSmpte(h, m, s, f); } diff --git a/aegisub/src/ass_time.h b/aegisub/src/ass_time.h index 0de545cc6..7e6f8c311 100644 --- a/aegisub/src/ass_time.h +++ b/aegisub/src/ass_time.h @@ -69,24 +69,19 @@ public: wxString GetASSFormated(bool ms=false) const; }; -/// DOCME -/// @class FractionalTime -/// @brief DOCME -/// -/// DOCME -class FractionalTime { +/// @class SmpteFormatter +/// @brief Convert times to and from SMPTE timecodes +class SmpteFormatter { + /// Frame rate to use agi::vfr::Framerate fps; - bool drop; ///< Enable SMPTE dropframe handling - - /// How often to drop frames when enabled - static const int frames_per_period = 17982; + /// Separator character + char sep; public: - FractionalTime(agi::vfr::Framerate fps, bool dropframe = false); - - bool IsDrop() const { return drop; } - agi::vfr::Framerate const& FPS() const { return fps; } + SmpteFormatter(agi::vfr::Framerate fps, char sep=':'); /// Convert an AssTime to a SMPTE timecode - wxString ToSMPTE(AssTime time, char sep=':'); + wxString ToSMPTE(AssTime time) const; + /// Convert a SMPTE timecode to an AssTime + AssTime FromSMPTE(wxString const& str) const; }; diff --git a/aegisub/src/subtitle_format.cpp b/aegisub/src/subtitle_format.cpp index e91ce4b0e..b44d4378d 100644 --- a/aegisub/src/subtitle_format.cpp +++ b/aegisub/src/subtitle_format.cpp @@ -100,20 +100,19 @@ bool SubtitleFormat::CanSave(const AssFile *subs) const { return true; } -FractionalTime SubtitleFormat::AskForFPS(bool showSMPTE) const { +agi::vfr::Framerate SubtitleFormat::AskForFPS(bool allow_vfr, bool show_smpte) const { wxArrayString choices; - bool drop = false; // Video FPS VideoContext *context = VideoContext::Get(); bool vidLoaded = context->TimecodesLoaded(); if (vidLoaded) { - wxString vidFPS; - if (context->FPS().IsVFR()) - vidFPS = "VFR"; + if (!context->FPS().IsVFR()) + choices.Add(wxString::Format(_("From video (%g)"), context->FPS().FPS())); + else if (allow_vfr) + choices.Add(_("From video (VFR)")); else - vidFPS = wxString::Format("%.3f", context->FPS().FPS()); - choices.Add(wxString::Format(_("From video (%s)"), vidFPS)); + vidLoaded = false; } // Standard FPS values @@ -122,7 +121,7 @@ FractionalTime SubtitleFormat::AskForFPS(bool showSMPTE) const { choices.Add(_("24.000 FPS (FILM)")); choices.Add(_("25.000 FPS (PAL)")); choices.Add(_("29.970 FPS (NTSC)")); - if (showSMPTE) + if (show_smpte) choices.Add(_("29.970 FPS (NTSC with SMPTE dropframe)")); choices.Add(_("30.000 FPS")); choices.Add(_("50.000 FPS (PAL x2)")); @@ -132,34 +131,35 @@ FractionalTime SubtitleFormat::AskForFPS(bool showSMPTE) const { choices.Add(_("120.000 FPS")); using agi::vfr::Framerate; - Framerate fps; // Ask int choice = wxGetSingleChoiceIndex(_("Please choose the appropriate FPS for the subtitles:"), _("FPS"), choices); if (choice == -1) - return FractionalTime(fps); + return Framerate(); // Get FPS from choice - if (vidLoaded) choice--; - // dropframe was displayed, that means all choices >4 are bumped up by 1 - if (!showSMPTE && choice > 4) ++choice; + if (vidLoaded) + --choice; + if (!show_smpte && choice > 4) + --choice; switch (choice) { - case -1: fps = context->FPS(); break; // VIDEO - case 0: fps = Framerate(15, 1); break; - case 1: fps = Framerate(24000, 1001); break; - case 2: fps = Framerate(24, 1); break; - case 3: fps = Framerate(25, 1); break; - case 4: fps = Framerate(30000, 1001); break; - case 5: fps = Framerate(30000, 1001); drop = true; break; - case 6: fps = Framerate(30, 1); break; - case 7: fps = Framerate(50, 1); break; - case 8: fps = Framerate(60000, 1001); break; - case 9: fps = Framerate(60, 1); break; - case 10: fps = Framerate(120000, 1001); break; - case 11: fps = Framerate(120, 1); break; + case -1: return context->FPS(); break; // VIDEO + case 0: return Framerate(15, 1); break; + case 1: return Framerate(24000, 1001); break; + case 2: return Framerate(24, 1); break; + case 3: return Framerate(25, 1); break; + case 4: return Framerate(30000, 1001); break; + case 5: return Framerate(30000, 1001, true); break; + case 6: return Framerate(30, 1); break; + case 7: return Framerate(50, 1); break; + case 8: return Framerate(60000, 1001); break; + case 9: return Framerate(60, 1); break; + case 10: return Framerate(120000, 1001); break; + case 11: return Framerate(120, 1); break; } - return FractionalTime(fps, drop); + assert(false); + return Framerate(); } void SubtitleFormat::StripTags(LineList &lines) const { diff --git a/aegisub/src/subtitle_format.h b/aegisub/src/subtitle_format.h index ae725743e..a43f59a63 100644 --- a/aegisub/src/subtitle_format.h +++ b/aegisub/src/subtitle_format.h @@ -47,7 +47,7 @@ class AssEntry; class AssFile; -class FractionalTime; +namespace agi { namespace vfr { class Framerate; } } /// DOCME /// @class SubtitleFormat @@ -85,9 +85,10 @@ protected: /// Merge sequential identical lines void MergeIdentical(LineList &lines) const; - /// Prompt the user for a framerate to use - /// @param showSMPTE Include SMPTE as an option? - FractionalTime AskForFPS(bool showSMPTE=false) const; + /// Prompt the user for a frame rate to use + /// @param allow_vfr Include video frame rate as an option even if it's vfr + /// @param show_smpte Show SMPTE drop frame option + agi::vfr::Framerate AskForFPS(bool allow_vfr, bool show_smpte) const; public: /// Constructor diff --git a/aegisub/src/subtitle_format_encore.cpp b/aegisub/src/subtitle_format_encore.cpp index c78605220..2300cee58 100644 --- a/aegisub/src/subtitle_format_encore.cpp +++ b/aegisub/src/subtitle_format_encore.cpp @@ -58,8 +58,8 @@ bool EncoreSubtitleFormat::CanWriteFile(wxString const& filename) const { } void EncoreSubtitleFormat::WriteFile(const AssFile *src, wxString const& filename, wxString const&) const { - FractionalTime ft = AskForFPS(true); - if (!ft.FPS().IsLoaded()) return; + agi::vfr::Framerate fps = AskForFPS(false, true); + if (!fps.IsLoaded()) return; // Convert to encore AssFile copy(*src); @@ -70,17 +70,19 @@ void EncoreSubtitleFormat::WriteFile(const AssFile *src, wxString const& filenam StripTags(copy.Line); ConvertNewlines(copy.Line, "\r\n"); + + // Encode wants ; for NTSC and : for PAL + // The manual suggests no other frame rates are supported + char sep = fps.NeedsDropFrames() ? ';' : ':'; + SmpteFormatter ft(fps, sep); + // Write lines int i = 0; - - // Encore wants ; instead of : if we're dealing with NTSC dropframe stuff - char sep = ft.IsDrop() ? ';' : ':'; - TextFileWriter file(filename, "UTF-8"); for (LineList::const_iterator cur = copy.Line.begin(); cur != copy.Line.end(); ++cur) { if (AssDialogue *current = dynamic_cast(*cur)) { ++i; - file.WriteLineToFile(wxString::Format("%i %s %s %s", i, ft.ToSMPTE(current->Start, sep), ft.ToSMPTE(current->End, sep), current->Text)); + file.WriteLineToFile(wxString::Format("%i %s %s %s", i, ft.ToSMPTE(current->Start), ft.ToSMPTE(current->End), current->Text)); } } } diff --git a/aegisub/src/subtitle_format_microdvd.cpp b/aegisub/src/subtitle_format_microdvd.cpp index 9fc493044..e141e41d7 100644 --- a/aegisub/src/subtitle_format_microdvd.cpp +++ b/aegisub/src/subtitle_format_microdvd.cpp @@ -109,7 +109,7 @@ void MicroDVDSubtitleFormat::ReadFile(AssFile *target, wxString const& filename, } // If it wasn't an fps line, ask the user for it - fps = AskForFPS().FPS(); + fps = AskForFPS(true, false); if (!fps.IsLoaded()) return; } @@ -125,7 +125,7 @@ void MicroDVDSubtitleFormat::ReadFile(AssFile *target, wxString const& filename, } void MicroDVDSubtitleFormat::WriteFile(const AssFile *src, wxString const& filename, wxString const& encoding) const { - agi::vfr::Framerate fps = AskForFPS().FPS(); + agi::vfr::Framerate fps = AskForFPS(true, false); if (!fps.IsLoaded()) return; AssFile copy(*src); diff --git a/aegisub/src/subtitle_format_transtation.cpp b/aegisub/src/subtitle_format_transtation.cpp index ae1fe7f9f..ce0aea961 100644 --- a/aegisub/src/subtitle_format_transtation.cpp +++ b/aegisub/src/subtitle_format_transtation.cpp @@ -64,10 +64,8 @@ bool TranStationSubtitleFormat::CanWriteFile(wxString const& filename) const { } void TranStationSubtitleFormat::WriteFile(const AssFile *src, wxString const& filename, wxString const& encoding) const { - FractionalTime ft = AskForFPS(true); - if (!ft.FPS().IsLoaded()) return; - - TextFileWriter file(filename, encoding); + agi::vfr::Framerate fps = AskForFPS(false, true); + if (!fps.IsLoaded()) return; // Convert to TranStation AssFile copy(*src); @@ -76,12 +74,14 @@ void TranStationSubtitleFormat::WriteFile(const AssFile *src, wxString const& fi RecombineOverlaps(copy.Line); MergeIdentical(copy.Line); + SmpteFormatter ft(fps); + TextFileWriter file(filename, encoding); AssDialogue *prev = 0; for (std::list::iterator it = copy.Line.begin(); it != copy.Line.end(); ++it) { AssDialogue *cur = dynamic_cast(*it); if (prev && cur) { - file.WriteLineToFile(ConvertLine(©, prev, &ft, cur->Start)); + file.WriteLineToFile(ConvertLine(©, prev, fps, ft, cur->Start)); file.WriteLineToFile(""); } @@ -91,13 +91,13 @@ void TranStationSubtitleFormat::WriteFile(const AssFile *src, wxString const& fi // flush last line if (prev) - file.WriteLineToFile(ConvertLine(©, prev, &ft, -1)); + file.WriteLineToFile(ConvertLine(©, prev, fps, ft, -1)); // Every file must end with this line file.WriteLineToFile("SUB["); } -wxString TranStationSubtitleFormat::ConvertLine(AssFile *file, AssDialogue *current, FractionalTime *ft, int nextl_start) const { +wxString TranStationSubtitleFormat::ConvertLine(AssFile *file, AssDialogue *current, agi::vfr::Framerate const& fps, SmpteFormatter const& ft, int nextl_start) const { int valign = 0; const char *halign = " "; // default is centered const char *type = "N"; // no special style @@ -120,9 +120,9 @@ wxString TranStationSubtitleFormat::ConvertLine(AssFile *file, AssDialogue *curr // start of next one, since the end timestamp is inclusive and the lines // would overlap if left as is. if (nextl_start > 0 && end == nextl_start) - end = ft->FPS().TimeAtFrame(ft->FPS().FrameAtTime(end, agi::vfr::END) - 1, agi::vfr::END); + end = fps.TimeAtFrame(fps.FrameAtTime(end, agi::vfr::END) - 1, agi::vfr::END); - wxString header = wxString::Format("SUB[%i%s%s ", valign, halign, type) + ft->ToSMPTE(current->Start) + ">" + ft->ToSMPTE(end) + "]\r\n"; + wxString header = wxString::Format("SUB[%i%s%s ", valign, halign, type) + ft.ToSMPTE(current->Start) + ">" + ft.ToSMPTE(end) + "]\r\n"; // Process text wxString lineEnd = "\r\n"; diff --git a/aegisub/src/subtitle_format_transtation.h b/aegisub/src/subtitle_format_transtation.h index 039bf2b1f..9defbc387 100644 --- a/aegisub/src/subtitle_format_transtation.h +++ b/aegisub/src/subtitle_format_transtation.h @@ -37,6 +37,7 @@ #include "subtitle_format.h" class AssDialogue; +class SmpteFormatter; /// DOCME /// @class TranStationSubtitleFormat @@ -44,7 +45,7 @@ class AssDialogue; /// /// DOCME class TranStationSubtitleFormat : public SubtitleFormat { - wxString ConvertLine(AssFile *file, AssDialogue *line, FractionalTime *ft, int nextl_start) const; + wxString ConvertLine(AssFile *file, AssDialogue *line, agi::vfr::Framerate const& fps, SmpteFormatter const& ft, int nextl_start) const; public: TranStationSubtitleFormat(); diff --git a/aegisub/tests/libaegisub_vfr.cpp b/aegisub/tests/libaegisub_vfr.cpp index 80ecc272f..212db9be6 100644 --- a/aegisub/tests/libaegisub_vfr.cpp +++ b/aegisub/tests/libaegisub_vfr.cpp @@ -445,3 +445,327 @@ TEST(lagi_vfr, duplicate_timestamps) { EXPECT_EQ(2, fps.FrameAtTime(199, EXACT)); EXPECT_EQ(3, fps.FrameAtTime(200, EXACT)); } + +#define EXPECT_SMPTE(eh, em, es, ef) \ + EXPECT_EQ(eh, h); \ + EXPECT_EQ(em, m); \ + EXPECT_EQ(es, s); \ + EXPECT_EQ(ef, f) + +TEST(lagi_vfr, to_smpte_ntsc) { + Framerate fps; + ASSERT_NO_THROW(fps = Framerate(30000, 1001)); + + EXPECT_TRUE(fps.NeedsDropFrames()); + + int h = -1, m = -1, s = -1, f = -1; + + ASSERT_NO_THROW(fps.SmpteAtFrame(0, &h, &m, &s, &f)); + EXPECT_SMPTE(0, 0, 0, 0); + + ASSERT_NO_THROW(fps.SmpteAtFrame(1, &h, &m, &s, &f)); + EXPECT_SMPTE(0, 0, 0, 1); + + ASSERT_NO_THROW(fps.SmpteAtFrame(29, &h, &m, &s, &f)); + EXPECT_SMPTE(0, 0, 0, 29); + + ASSERT_NO_THROW(fps.SmpteAtFrame(30, &h, &m, &s, &f)); + EXPECT_SMPTE(0, 0, 1, 0); + + ASSERT_NO_THROW(fps.SmpteAtFrame(1799, &h, &m, &s, &f)); + EXPECT_SMPTE(0, 0, 59, 29); + + ASSERT_NO_THROW(fps.SmpteAtFrame(1800, &h, &m, &s, &f)); + EXPECT_SMPTE(0, 1, 0, 2); + + ASSERT_NO_THROW(fps.SmpteAtFrame(3597, &h, &m, &s, &f)); + EXPECT_SMPTE(0, 1, 59, 29); + + ASSERT_NO_THROW(fps.SmpteAtFrame(3598, &h, &m, &s, &f)); + EXPECT_SMPTE(0, 2, 0, 2); + + ASSERT_NO_THROW(fps.SmpteAtFrame(5396, &h, &m, &s, &f)); + EXPECT_SMPTE(0, 3, 0, 2); + + ASSERT_NO_THROW(fps.SmpteAtFrame(7194, &h, &m, &s, &f)); + EXPECT_SMPTE(0, 4, 0, 2); + + ASSERT_NO_THROW(fps.SmpteAtFrame(107892, &h, &m, &s, &f)); + EXPECT_SMPTE(1, 0, 0, 0); + + for (int i = 0; i < 60 * 60 * 10; ++i) { + ASSERT_NO_THROW(fps.SmpteAtTime(i * 1000, &h, &m, &s, &f)); + ASSERT_NEAR(i, h * 3600 + m * 60 + s, 1); + } +} + +TEST(lagi_vfr, to_smpte_double_ntsc) { + Framerate fps; + ASSERT_NO_THROW(fps = Framerate(60000, 1001)); + + EXPECT_TRUE(fps.NeedsDropFrames()); + + int h = -1, m = -1, s = -1, f = -1; + + ASSERT_NO_THROW(fps.SmpteAtFrame(0, &h, &m, &s, &f)); + EXPECT_SMPTE(0, 0, 0, 0); + + ASSERT_NO_THROW(fps.SmpteAtFrame(1, &h, &m, &s, &f)); + EXPECT_SMPTE(0, 0, 0, 1); + + ASSERT_NO_THROW(fps.SmpteAtFrame(59, &h, &m, &s, &f)); + EXPECT_SMPTE(0, 0, 0, 59); + + ASSERT_NO_THROW(fps.SmpteAtFrame(60, &h, &m, &s, &f)); + EXPECT_SMPTE(0, 0, 1, 0); + + ASSERT_NO_THROW(fps.SmpteAtFrame(3599, &h, &m, &s, &f)); + EXPECT_SMPTE(0, 0, 59, 59); + + ASSERT_NO_THROW(fps.SmpteAtFrame(3600, &h, &m, &s, &f)); + EXPECT_SMPTE(0, 1, 0, 4); + + ASSERT_NO_THROW(fps.SmpteAtFrame(7195, &h, &m, &s, &f)); + EXPECT_SMPTE(0, 1, 59, 59); + + ASSERT_NO_THROW(fps.SmpteAtFrame(7196, &h, &m, &s, &f)); + EXPECT_SMPTE(0, 2, 0, 4); + + ASSERT_NO_THROW(fps.SmpteAtFrame(215784, &h, &m, &s, &f)); + EXPECT_SMPTE(1, 0, 0, 0); + + for (int i = 0; i < 60 * 60 * 10; ++i) { + ASSERT_NO_THROW(fps.SmpteAtTime(i * 1000, &h, &m, &s, &f)); + ASSERT_NEAR(i, h * 3600 + m * 60 + s, 1); + } +} + +TEST(lagi_vfr, to_smpte_pal) { + Framerate fps; + ASSERT_NO_THROW(fps = Framerate(25, 1)); + + EXPECT_FALSE(fps.NeedsDropFrames()); + + int h = -1, m = -1, s = -1, f = -1; + + ASSERT_NO_THROW(fps.SmpteAtFrame(0, &h, &m, &s, &f)); + EXPECT_SMPTE(0, 0, 0, 0); + + ASSERT_NO_THROW(fps.SmpteAtFrame(1, &h, &m, &s, &f)); + EXPECT_SMPTE(0, 0, 0, 1); + + ASSERT_NO_THROW(fps.SmpteAtFrame(24, &h, &m, &s, &f)); + EXPECT_SMPTE(0, 0, 0, 24); + + ASSERT_NO_THROW(fps.SmpteAtFrame(25, &h, &m, &s, &f)); + EXPECT_SMPTE(0, 0, 1, 0); + + ASSERT_NO_THROW(fps.SmpteAtFrame(1499, &h, &m, &s, &f)); + EXPECT_SMPTE(0, 0, 59, 24); + + ASSERT_NO_THROW(fps.SmpteAtFrame(1500, &h, &m, &s, &f)); + EXPECT_SMPTE(0, 1, 0, 0); + + ASSERT_NO_THROW(fps.SmpteAtFrame(25 * 60 * 60, &h, &m, &s, &f)); + EXPECT_SMPTE(1, 0, 0, 0); + + for (int i = 0; i < 60 * 60 * 10; ++i) { + ASSERT_NO_THROW(fps.SmpteAtTime(i * 1000, &h, &m, &s, &f)); + ASSERT_EQ(i, h * 3600 + m * 60 + s); + } +} + +// this test is different from the above due to that the exact frames which are +// skipped are undefined, so instead it tests that the error never exceeds the +// limit +TEST(lagi_vfr, to_smpte_decimated) { + Framerate fps; + ASSERT_NO_THROW(fps = Framerate(24000, 1001)); + + EXPECT_TRUE(fps.NeedsDropFrames()); + + int h = -1, m = -1, s = -1, f = -1; + + ASSERT_NO_THROW(fps.SmpteAtFrame(0, &h, &m, &s, &f)); + EXPECT_SMPTE(0, 0, 0, 0); + + ASSERT_NO_THROW(fps.SmpteAtFrame(1, &h, &m, &s, &f)); + EXPECT_SMPTE(0, 0, 0, 1); + + for (int frame = 0; frame < 100000; ++frame) { + ASSERT_NO_THROW(fps.SmpteAtFrame(frame, &h, &m, &s, &f)); + int expected_time = fps.TimeAtFrame(frame); + int real_time = int((h * 3600 + m * 60 + s + f / 24.0) * 1000.0); + ASSERT_NEAR(expected_time, real_time, 600.0 / fps.FPS()); + } +} + +TEST(lagi_vfr, to_smpte_manydrop) { + Framerate fps; + ASSERT_NO_THROW(fps = Framerate(24, 11)); + + EXPECT_TRUE(fps.NeedsDropFrames()); + + int h = -1, m = -1, s = -1, f = -1; + + ASSERT_NO_THROW(fps.SmpteAtFrame(0, &h, &m, &s, &f)); + EXPECT_SMPTE(0, 0, 0, 0); + + ASSERT_NO_THROW(fps.SmpteAtFrame(1, &h, &m, &s, &f)); + EXPECT_SMPTE(0, 0, 0, 1); + + for (int frame = 0; frame < 1000; ++frame) { + ASSERT_NO_THROW(fps.SmpteAtFrame(frame, &h, &m, &s, &f)); + int expected_time = fps.TimeAtFrame(frame); + int real_time = int((h * 3600 + m * 60 + s + f / 3.0) * 1000.0); + ASSERT_NEAR(expected_time, real_time, 600.0 / fps.FPS()); + } +} + +TEST(lagi_vfr, from_smpte_ntsc) { + Framerate fps; + ASSERT_NO_THROW(fps = Framerate(30000, 1001)); + + EXPECT_EQ(0, fps.FrameAtSmpte(0, 0, 0, 0)); + EXPECT_EQ(1, fps.FrameAtSmpte(0, 0, 0, 1)); + EXPECT_EQ(29, fps.FrameAtSmpte(0, 0, 0, 29)); + EXPECT_EQ(30, fps.FrameAtSmpte(0, 0, 1, 0)); + EXPECT_EQ(1799, fps.FrameAtSmpte(0, 0, 59, 29)); + EXPECT_EQ(1800, fps.FrameAtSmpte(0, 1, 0, 0)); + EXPECT_EQ(1800, fps.FrameAtSmpte(0, 1, 0, 1)); + EXPECT_EQ(1800, fps.FrameAtSmpte(0, 1, 0, 2)); + EXPECT_EQ(3597, fps.FrameAtSmpte(0, 1, 59, 29)); + EXPECT_EQ(3598, fps.FrameAtSmpte(0, 2, 0, 0)); + EXPECT_EQ(3598, fps.FrameAtSmpte(0, 2, 0, 1)); + EXPECT_EQ(3598, fps.FrameAtSmpte(0, 2, 0, 2)); + EXPECT_EQ(5396, fps.FrameAtSmpte(0, 3, 0, 2)); + EXPECT_EQ(7194, fps.FrameAtSmpte(0, 4, 0, 2)); + EXPECT_EQ(107892, fps.FrameAtSmpte(1, 0, 0, 0)); +} + +TEST(lagi_vfr, from_smpte_double_ntsc) { + Framerate fps; + ASSERT_NO_THROW(fps = Framerate(60000, 1001)); + + EXPECT_TRUE(fps.NeedsDropFrames()); + + EXPECT_EQ(0, fps.FrameAtSmpte(0, 0, 0, 0)); + EXPECT_EQ(1, fps.FrameAtSmpte(0, 0, 0, 1)); + EXPECT_EQ(59, fps.FrameAtSmpte(0, 0, 0, 59)); + EXPECT_EQ(60, fps.FrameAtSmpte(0, 0, 1, 0)); + EXPECT_EQ(3599, fps.FrameAtSmpte(0, 0, 59, 59)); + EXPECT_EQ(3600, fps.FrameAtSmpte(0, 1, 0, 4)); + EXPECT_EQ(7195, fps.FrameAtSmpte(0, 1, 59, 59)); + EXPECT_EQ(7196, fps.FrameAtSmpte(0, 2, 0, 4)); + EXPECT_EQ(10792, fps.FrameAtSmpte(0, 3, 0, 0)); + EXPECT_EQ(10792, fps.FrameAtSmpte(0, 3, 0, 1)); + EXPECT_EQ(10792, fps.FrameAtSmpte(0, 3, 0, 2)); + EXPECT_EQ(10792, fps.FrameAtSmpte(0, 3, 0, 3)); + EXPECT_EQ(10792, fps.FrameAtSmpte(0, 3, 0, 4)); + EXPECT_EQ(10793, fps.FrameAtSmpte(0, 3, 0, 5)); + EXPECT_EQ(215784, fps.FrameAtSmpte(1, 0, 0, 0)); +} + +TEST(lagi_vfr, from_smpte_pal) { + Framerate fps; + ASSERT_NO_THROW(fps = Framerate(25, 1)); + + EXPECT_FALSE(fps.NeedsDropFrames()); + + EXPECT_EQ(0, fps.FrameAtSmpte(0, 0, 0, 0)); + EXPECT_EQ(1, fps.FrameAtSmpte(0, 0, 0, 1)); + EXPECT_EQ(24, fps.FrameAtSmpte(0, 0, 0, 24)); + EXPECT_EQ(25, fps.FrameAtSmpte(0, 0, 1, 0)); + EXPECT_EQ(1499, fps.FrameAtSmpte(0, 0, 59, 24)); + EXPECT_EQ(1500, fps.FrameAtSmpte(0, 1, 0, 0)); + EXPECT_EQ(25 * 60 * 60, fps.FrameAtSmpte(1, 0, 0, 0)); +} + +TEST(lagi_vfr, roundtrip_smpte_ntsc) { + Framerate fps; + ASSERT_NO_THROW(fps = Framerate(30000, 1001)); + + int h = -1, m = -1, s = -1, f = -1; + + for (int i = 0; i < 100000; ++i) { + ASSERT_NO_THROW(fps.SmpteAtFrame(i, &h, &m, &s, &f)); + ASSERT_EQ(i, fps.FrameAtSmpte(h, m, s, f)); + } +} + +TEST(lagi_vfr, roundtrip_smpte_pal) { + Framerate fps; + ASSERT_NO_THROW(fps = Framerate(25, 1)); + + int h = -1, m = -1, s = -1, f = -1; + + for (int i = 0; i < 100000; ++i) { + ASSERT_NO_THROW(fps.SmpteAtFrame(i, &h, &m, &s, &f)); + ASSERT_EQ(i, fps.FrameAtSmpte(h, m, s, f)); + } +} + +TEST(lagi_vfr, roundtrip_smpte_manydrop) { + Framerate fps; + ASSERT_NO_THROW(fps = Framerate(20, 11)); + + int h = -1, m = -1, s = -1, f = -1; + + for (int i = 0; i < 10000; ++i) { + ASSERT_NO_THROW(fps.SmpteAtFrame(i, &h, &m, &s, &f)); + ASSERT_EQ(i, fps.FrameAtSmpte(h, m, s, f)); + } +} + +TEST(lagi_vfr, roundtrip_smpte_decimated) { + Framerate fps; + ASSERT_NO_THROW(fps = Framerate(24000, 1001)); + + int h = -1, m = -1, s = -1, f = -1; + + for (int i = 0; i < 100000; ++i) { + ASSERT_NO_THROW(fps.SmpteAtFrame(i, &h, &m, &s, &f)); + ASSERT_EQ(i, fps.FrameAtSmpte(h, m, s, f)); + } +} + +TEST(lagi_vfr, to_smpte_ntsc_nodrop) { + Framerate fps; + ASSERT_NO_THROW(fps = Framerate(30000, 1001, false)); + + int h = -1, m = -1, s = -1, f = -1; + + for (int i = 0; i < 100000; ++i) { + ASSERT_NO_THROW(fps.SmpteAtFrame(i, &h, &m, &s, &f)); + ASSERT_EQ(i, h * 60 * 60 * 30 + m * 60 * 30 + s * 30 + f); + } +} + +TEST(lagi_vfr, from_smpte_ntsc_nodrop) { + Framerate fps; + ASSERT_NO_THROW(fps = Framerate(30000, 1001, false)); + + int h = 0, m = 0, s = 0, f = 0; + + int i = 0; + while (h < 10) { + if (f >= 30) { + f = 0; + ++s; + } + + if (s >= 60) { + s = 0; + ++m; + } + + if (m >= 60) { + m = 0; + ++h; + } + + ASSERT_EQ(i, fps.FrameAtSmpte(h, m, s, f)); + ++i; + ++f; + } +}