diff --git a/aegisub/build/libaegisub_vs2008/libaegisub_vs2008.vcproj b/aegisub/build/libaegisub_vs2008/libaegisub_vs2008.vcproj
index ad6146e7e..af1f677b0 100644
--- a/aegisub/build/libaegisub_vs2008/libaegisub_vs2008.vcproj
+++ b/aegisub/build/libaegisub_vs2008/libaegisub_vs2008.vcproj
@@ -226,6 +226,10 @@
RelativePath="..\..\libaegisub\common\validator.cpp"
>
+
+
+
+
@@ -384,6 +392,10 @@
RelativePath="..\..\libaegisub\include\libaegisub\validator.h"
>
+
+
+
+
diff --git a/aegisub/libaegisub/Makefile.am b/aegisub/libaegisub/Makefile.am
index dfd2a7ae1..ae19ae598 100644
--- a/aegisub/libaegisub/Makefile.am
+++ b/aegisub/libaegisub/Makefile.am
@@ -30,6 +30,7 @@ libaegisub_2_2_la_SOURCES = \
common/option_visit.cpp \
common/log.cpp \
common/validator.cpp \
+ common/vfr.cpp \
unix/util.cpp \
unix/io.cpp \
unix/access.cpp \
diff --git a/aegisub/libaegisub/common/vfr.cpp b/aegisub/libaegisub/common/vfr.cpp
new file mode 100644
index 000000000..039214f4d
--- /dev/null
+++ b/aegisub/libaegisub/common/vfr.cpp
@@ -0,0 +1,307 @@
+// Copyright (c) 2010, Thomas Goyne
+//
+// Permission to use, copy, modify, and distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+//
+// $Id$
+
+/// @file vfr.cpp
+/// @brief Framerate handling of all sorts
+/// @ingroup libaegisub video_input
+
+#include "libaegisub/vfr.h"
+
+#ifndef LAGI_PRE
+#include
+#include
+#include
+#include
+#endif
+
+#include "libaegisub/charset.h"
+#include "libaegisub/io.h"
+#include "libaegisub/line_iterator.h"
+
+namespace std {
+ template<> void swap(agi::vfr::Framerate &lft, agi::vfr::Framerate &rgt) throw() {
+ lft.swap(rgt);
+ }
+}
+
+namespace agi {
+namespace vfr {
+
+static int is_increasing(int prev, int cur) {
+ if (prev >= cur) {
+ throw UnorderedTimecodes("Timecodes are out of order or too close together");
+ }
+ return cur;
+}
+
+/// @brief Verify that timecodes monotonically increase
+/// @param timecodes List of timecodes to check
+static void validate_timecodes(std::vector const& timecodes) {
+ if (timecodes.size() <= 1) {
+ throw TooFewTimecodes("Must have at least two timecodes to do anything useful");
+ }
+ std::accumulate(timecodes.begin()+1, timecodes.end(), timecodes.front(), is_increasing);
+}
+
+// A "start,end,fps" line in a v1 timecode file
+struct TimecodeRange {
+ int start;
+ int end;
+ double fps;
+ double time;
+ bool operator<(TimecodeRange cmp) {
+ return start < cmp.start;
+ }
+ TimecodeRange() : fps(0.) { }
+};
+
+/// @brief Parse a single line of a v1 timecode file
+/// @param str Line to parse
+/// @return The line in TimecodeRange form, or TimecodeRange() if it's a comment
+static TimecodeRange v1_parse_line(std::string const& str) {
+ if (str.empty() || str[0] == '#') return TimecodeRange();
+
+ std::istringstream ss(str);
+ TimecodeRange range;
+ char comma1, comma2;
+ ss >> range.start >> comma1 >> range.end >> comma2 >> range.fps;
+ if (ss.fail() || comma1 != ',' || comma2 != ',' || !ss.eof()) {
+ throw MalformedLine(str);
+ }
+ if (range.start < 0 || range.end < 0) {
+ throw UnorderedTimecodes("Cannot specify frame rate for negative frames.");
+ }
+ if (range.end < range.start) {
+ throw UnorderedTimecodes("End frame must be greater than or equal to start frame");
+ }
+ if (range.fps <= 0.) {
+ throw BadFPS("FPS must be greater than zero");
+ }
+ if (range.fps > 1000.) {
+ // This is our limitation, not mkvmerge's
+ // mkvmerge uses nanoseconds internally
+ throw BadFPS("FPS must be at most 1000");
+ }
+ return range;
+}
+
+/// @brief Is the timecode range a comment line?
+static bool v1_invalid_timecode(TimecodeRange const& range) {
+ return range.fps == 0.;
+}
+
+/// @brief Generate override ranges for all frames with assumed fpses
+/// @param ranges List with ranges which is mutated
+/// @param fps Assumed fps to use for gaps
+static void v1_fill_range_gaps(std::list &ranges, double fps) {
+ // Range for frames between start and first override
+ if (ranges.empty() || ranges.front().start > 0) {
+ TimecodeRange range;
+ range.fps = fps;
+ range.start = 0;
+ range.end = ranges.empty() ? 0 : ranges.front().start - 1;
+ ranges.push_front(range);
+ }
+ std::list::iterator cur = ++ranges.begin();
+ std::list::iterator prev = ranges.begin();
+ for (; cur != ranges.end(); ++cur, ++prev) {
+ if (prev->end >= cur->start) {
+ // mkvmerge allows overlapping timecode ranges, but does completely
+ // broken things with them
+ throw UnorderedTimecodes("Override ranges must not overlap");
+ }
+ if (prev->end + 1 < cur->start) {
+ TimecodeRange range;
+ range.fps = fps;
+ range.start = prev->end + 1;
+ range.end = cur->start - 1;
+ ranges.insert(cur, range);
+ ++prev;
+ }
+ }
+}
+
+/// @brief Parse a v1 timecode file
+/// @param file Iterator of lines in the file
+/// @param line Header of file with assumed fps
+/// @param[out] timecodes Vector filled with frame start times
+/// @param[out] time Unrounded time of the last frame
+/// @return Assumed fps
+static double v1_parse(line_iterator file, std::string line, std::vector &timecodes, double &time) {
+ using namespace std;
+ double fps = atof(line.substr(7).c_str());
+ if (fps <= 0.) throw BadFPS("Assumed FPS must be greater than zero");
+ if (fps > 1000.) throw BadFPS("Assumed FPS must not be greater than 1000");
+
+ list ranges;
+ transform(file, line_iterator(), back_inserter(ranges), v1_parse_line);
+ ranges.erase(remove_if(ranges.begin(), ranges.end(), v1_invalid_timecode), ranges.end());
+
+ ranges.sort();
+ v1_fill_range_gaps(ranges, fps);
+ timecodes.reserve(ranges.back().end);
+
+ time = 0.;
+ for (list::iterator cur = ranges.begin(); cur != ranges.end(); ++cur) {
+ for (int frame = cur->start; frame <= cur->end; frame++) {
+ timecodes.push_back(int(time + .5));
+ time += 1000. / cur->fps;
+ }
+ }
+ timecodes.push_back(int(time + .5));
+ return fps;
+}
+
+Framerate::Framerate(Framerate const& that)
+: fps(that.fps)
+, last(that.last)
+, timecodes(that.timecodes)
+{
+}
+
+Framerate::Framerate(double fps) : fps(fps), last(0.) {
+ if (fps < 0.) throw BadFPS("FPS must be greater than zero");
+ if (fps > 1000.) throw BadFPS("FPS must not be greater than 1000");
+}
+
+Framerate::Framerate(std::vector const& timecodes)
+: timecodes(timecodes)
+{
+ validate_timecodes(timecodes);
+ fps = timecodes.size() / (timecodes.back() / 1000.);
+ last = timecodes.back();
+}
+
+Framerate::~Framerate() {
+}
+
+void Framerate::swap(Framerate &right) throw() {
+ std::swap(fps, right.fps);
+ std::swap(last, right.last);
+ std::swap(timecodes, right.timecodes);
+}
+
+Framerate &Framerate::operator=(Framerate right) {
+ std::swap(*this, right);
+ return *this;
+}
+Framerate &Framerate::operator=(double fps) {
+ return *this = Framerate(fps);
+}
+
+bool Framerate::operator==(Framerate const& right) const {
+ return fps == right.fps && timecodes == right.timecodes;
+}
+
+Framerate::Framerate(std::string const& filename) : fps(0.) {
+ using namespace std;
+ auto_ptr file(agi::io::Open(filename));
+ string encoding = agi::charset::Detect(filename);
+ string line = *line_iterator(*file, encoding);
+ if (line == "# timecode format v2") {
+ copy(line_iterator(*file, encoding), line_iterator(), back_inserter(timecodes));
+ validate_timecodes(timecodes);
+ fps = timecodes.size() / (timecodes.back() / 1000.);
+ return;
+ }
+ if (line == "# timecode format v1" || line.substr(0, 7) == "Assume ") {
+ if (line[0] == '#') {
+ line = *line_iterator(*file, encoding);
+ }
+ fps = v1_parse(line_iterator(*file, encoding), line, timecodes, last);
+ return;
+ }
+
+ throw UnknownFormat(line);
+}
+
+void Framerate::Save(std::string const& filename, int length) const {
+ agi::io::Save file(filename);
+ std::ofstream &out = file.Get();
+
+ out << "# timecode format v2\n";
+ std::copy(timecodes.begin(), timecodes.end(), std::ostream_iterator(out, "\n"));
+ for (int written = timecodes.size(); written < length; ++written) {
+ out << TimeAtFrame(written) << std::endl;
+ }
+}
+
+static int round(double value) {
+ return int(value + .5);
+}
+
+int Framerate::FrameAtTime(int ms, Time type) const {
+ // With X ms per frame, this should return 0 for:
+ // EXACT: [0, X]
+ // START: [-X, 0]
+ // END: [1, X + 1]
+
+ // There are two properties we take advantage of here:
+ // 1. START and END's ranges are adjacent, meaning doing the calculations
+ // for END and adding one gives us START
+ // 2. END is EXACT plus one ms, meaning we can subtract one ms to get EXACT
+
+ // Combining these allows us to easily calculate START and END in terms of
+ // EXACT
+
+ if (type == START) {
+ return FrameAtTime(ms - 1) + 1;
+ }
+ if (type == END) {
+ return FrameAtTime(ms - 1);
+ }
+
+ if (timecodes.empty()) {
+ return (int)floor(ms * fps / 1000.);
+ }
+ if (ms < timecodes.front()) {
+ return (int)floor((ms - timecodes.front()) * fps / 1000.);
+ }
+ if (ms > timecodes.back()) {
+ return round((ms - timecodes.back()) * fps / 1000.) + timecodes.size() - 1;
+ }
+
+ return std::distance(std::lower_bound(timecodes.rbegin(), timecodes.rend(), ms, std::greater()), timecodes.rend()) - 1;
+}
+
+int Framerate::TimeAtFrame(int frame, Time type) const {
+ if (type == START) {
+ int prev = TimeAtFrame(frame - 1);
+ int cur = TimeAtFrame(frame);
+ // + 1 as these need to round up for the case of two frames 1 ms apart
+ return prev + (cur - prev + 1) / 2;
+ }
+ if (type == END) {
+ int cur = TimeAtFrame(frame);
+ int next = TimeAtFrame(frame + 1);
+ return cur + (next - cur + 1) / 2;
+ }
+
+ if (timecodes.empty()) {
+ return (int)ceil(frame / fps * 1000.);
+ }
+
+ if (frame < 0) {
+ return (int)ceil(frame / fps * 1000.) + timecodes.front();
+ }
+ if (frame >= (signed)timecodes.size()) {
+ return round((frame - timecodes.size() + 1) * 1000. / fps + last);
+ }
+ return timecodes[frame];
+}
+
+}
+}
diff --git a/aegisub/libaegisub/include/libaegisub/line_iterator.h b/aegisub/libaegisub/include/libaegisub/line_iterator.h
index cf0c5b2a4..0761b7bf6 100644
--- a/aegisub/libaegisub/include/libaegisub/line_iterator.h
+++ b/aegisub/libaegisub/include/libaegisub/line_iterator.h
@@ -38,7 +38,7 @@ namespace agi {
/// @class line_iterator
/// @brief An iterator over lines in a stream
-template
+template
class line_iterator : public std::iterator {
std::istream *stream; ///< Stream to iterator over
bool valid; ///< Are there any more values to read?
@@ -151,7 +151,7 @@ public:
}
};
-template
+template
void line_iterator::getline(std::string &str) {
union {
int32_t chr;
@@ -180,7 +180,7 @@ void line_iterator::getline(std::string &str) {
}
}
-template
+template
void line_iterator::next() {
if (!valid) return;
if (!stream->good()) {
@@ -198,7 +198,7 @@ void line_iterator::next() {
}
}
-template
+template
inline bool line_iterator::convert(std::string &str) {
std::istringstream ss(str);
ss >> value;
@@ -210,7 +210,7 @@ inline bool line_iterator::convert(std::string &str) {
return true;
}
-template
+template
void swap(agi::line_iterator &lft, agi::line_iterator &rgt) {
lft.swap(rgt);
}
diff --git a/aegisub/libaegisub/include/libaegisub/vfr.h b/aegisub/libaegisub/include/libaegisub/vfr.h
new file mode 100644
index 000000000..046010b86
--- /dev/null
+++ b/aegisub/libaegisub/include/libaegisub/vfr.h
@@ -0,0 +1,142 @@
+// Copyright (c) 2010, Thomas Goyne
+//
+// Permission to use, copy, modify, and distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+//
+// $Id$
+
+/// @file vfr.h
+/// @brief Framerate handling of all sorts
+/// @ingroup libaegisub video_input
+
+#pragma once
+
+#if !defined(AGI_PRE) && !defined(LAGI_PRE)
+#include
+#include
+#endif
+
+#include
+
+namespace agi {
+namespace vfr {
+
+enum Time {
+ /// Use the actual frame times
+ /// With 1 FPS video, frame 0 is [0, 999] ms
+ EXACT,
+ /// Calculate based on the rules for start times of lines
+ /// Lines are first visible on the first frame with start time less than
+ /// or equal to the start time; thus with 1.0 FPS video, frame 0 is
+ /// [-999, 0] ms
+ START,
+ /// Calculate based on the rules for end times of lines
+ /// Lines are last visible on the last frame with start time less than the
+ /// end time; thus with 1.0 FPS video, frame 0 is [1, 1000] ms
+ END
+};
+
+DEFINE_BASE_EXCEPTION_NOINNER(Error, Exception)
+/// FPS specified is not a valid frame rate
+DEFINE_SIMPLE_EXCEPTION_NOINNER(BadFPS, Error, "vfr/badfps")
+/// Unknown timecode file format
+DEFINE_SIMPLE_EXCEPTION_NOINNER(UnknownFormat, Error, "vfr/timecodes/unknownformat")
+/// Invalid line encountered in a timecode file
+DEFINE_SIMPLE_EXCEPTION_NOINNER(MalformedLine, Error, "vfr/timecodes/malformed")
+/// Timecode file or vector has too few timecodes to be usable
+DEFINE_SIMPLE_EXCEPTION_NOINNER(TooFewTimecodes, Error, "vfr/timecodes/toofew")
+/// Timecode file or vector has timecodes that are not monotonically increasing
+DEFINE_SIMPLE_EXCEPTION_NOINNER(UnorderedTimecodes, Error, "vfr/timecodes/order")
+
+/// @class Framerate
+/// @brief Class for managing everything related to converting frames to times
+/// or vice versa
+class Framerate {
+ /// Average FPS for v2, assumed FPS for v1, fps for CFR
+ double fps;
+ /// Unrounded time of the last frame in a v1 override range. Needed to
+ /// match mkvmerge's rounding
+ double last;
+ /// Start time in milliseconds of each frame
+ std::vector timecodes;
+public:
+ /// Copy constructor
+ Framerate(Framerate const&);
+ /// @brief VFR from timecodes file
+ /// @param filename File with v1 or v2 timecodes
+ ///
+ /// Note that loading a v1 timecode file with Assume X and no overrides is
+ /// not the same thing as CFR X. When timecodes are loaded from a file,
+ /// mkvmerge-style rounding is applied, while setting a constant frame rate
+ /// uses truncation.
+ Framerate(std::string const& filename);
+ /// @brief CFR constructor
+ /// @param fps Frames per second or 0 for unloaded
+ Framerate(double fps = 0.);
+ /// @brief VFR from frame times
+ /// @param timecodes Vector of frame start times in milliseconds
+ Framerate(std::vector const& timecodes);
+ ~Framerate();
+ /// Atomic assignment operator
+ Framerate &operator=(Framerate);
+ /// Atomic CFR assignment operator
+ Framerate &operator=(double);
+ /// Helper function for the std::swap specialization
+ void swap(Framerate &right) throw();
+
+ /// @brief Get the frame visible at a given time
+ /// @param ms Time in milliseconds
+ /// @param type Time mode
+ ///
+ /// When type is EXACT, the frame returned is the frame visible at the given
+ /// time; when it is START or END it is the frame on which a line with that
+ /// start/end time would first/last be visible
+ int FrameAtTime(int ms, Time type = EXACT) const;
+
+ /// @brief Get the time at a given frame
+ /// @param frame Frame number
+ /// @param type Time mode
+ ///
+ /// When type is EXACT, the frame's exact start time is returned; START and
+ /// END give a time somewhere within the range that will result in the line
+ /// starting/ending on that frame
+ ///
+ /// With v2 timecodes, frames outside the defined range are not an error
+ /// and are guaranteed to be monotonically increasing/decreasing values
+ /// which when passed to FrameAtTime will return the original frame; they
+ /// are not guaranteed to be sensible or useful for any other purpose
+ ///
+ /// v1 timecodes and CFR do not have a defined range, and will give sensible
+ /// results for all frame numbers
+ int TimeAtFrame(int frame, Time type = EXACT) 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
+ ///
+ /// The length parameter is only particularly useful for v1 timecodes (and
+ /// CFR, but saving CFR timecodes is a bit silly). Extra timecodes generated
+ /// to hit length with v2 timecodes will monotonically increase but may not
+ /// be otherwise sensible.
+ void Save(std::string const& file, int length = -1) const;
+
+ bool IsVFR() const {return !timecodes.empty(); }
+ bool IsLoaded() const { return !timecodes.empty() || fps; };
+ double FPS() const { return fps; }
+
+ /// @brief Equality operator
+ /// @attention O(n) when both arguments are VFR
+ bool operator==(Framerate const& right) const;
+};
+
+}
+}
diff --git a/aegisub/libaegisub/lagi_pre.h b/aegisub/libaegisub/lagi_pre.h
index 1fbcc7ebe..beb6e2200 100644
--- a/aegisub/libaegisub/lagi_pre.h
+++ b/aegisub/libaegisub/lagi_pre.h
@@ -27,6 +27,8 @@
#include
#include
#include
+#include
+#include
#include