Aegisub/aegisub/libaegisub/common/vfr.cpp
Thomas Goyne acba2c6b63 Rewrite VFR handling in Aegisub.
Kill vfr.h and vfr.cpp and use the libaegisub versions of them instead.

Rather than the globals VFR_Input and VFR_Output, everything related to
frame rate is now part of the video context. Most things which used to
use VFR_Output now call VideoContext::TimeAtFrame etc.; video providers,
rather than modifying VFR_Input directly, now have getters for their
frame rates which VideoContext calls. Read-only public access to
VFR_Input and VFR_Output are still provided (hopefully temporarily) for
a few things which were awkward to do through VideoContext.

The Avisynth provider now might correctly handle VFR MKVs which can be
opened with DirectShowSource but not DSS2.

Rework keyframe handling as well, so that it continues to match the vfr
handling in design and implementation.

Originally committed to SVN as r4662.
2010-07-08 04:29:04 +00:00

304 lines
9.1 KiB
C++

// Copyright (c) 2010, Thomas Goyne <plorkyeran@aegisub.org>
//
// 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 <algorithm>
#include <functional>
#include <iterator>
#include <numeric>
#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<int> 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<TimecodeRange> &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<TimecodeRange>::iterator cur = ++ranges.begin();
std::list<TimecodeRange>::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<std::string> file, std::string line, std::vector<int> &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<TimecodeRange> ranges;
transform(file, line_iterator<string>(), 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<TimecodeRange>::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<int> const& timecodes)
: timecodes(timecodes)
{
validate_timecodes(timecodes);
fps = (timecodes.size() - 1) * 1000. / (timecodes.back() - timecodes.front());
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);
}
Framerate::Framerate(std::string const& filename) : fps(0.) {
using namespace std;
auto_ptr<ifstream> file(agi::io::Open(filename));
string encoding = agi::charset::Detect(filename);
string line = *line_iterator<string>(*file, encoding);
if (line == "# timecode format v2") {
copy(line_iterator<int>(*file, encoding), line_iterator<int>(), back_inserter(timecodes));
validate_timecodes(timecodes);
fps = (timecodes.size() - 1) * 1000. / (timecodes.back() - timecodes.front());
last = timecodes.back();
return;
}
if (line == "# timecode format v1" || line.substr(0, 7) == "Assume ") {
if (line[0] == '#') {
line = *line_iterator<string>(*file, encoding);
}
fps = v1_parse(line_iterator<string>(*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<int>(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<int>()), 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];
}
}
}