mirror of https://github.com/odrling/Aegisub
317 lines
9.5 KiB
C++
317 lines
9.5 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 <cmath>
|
|
#include <functional>
|
|
#include <iterator>
|
|
#include <list>
|
|
#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);
|
|
}
|
|
|
|
/// @brief Shift timecodes so that frame 0 starts at time 0
|
|
/// @param timecodes List of timecodes to normalize
|
|
static void normalize_timecodes(std::vector<int> &timecodes) {
|
|
if (int front = timecodes.front()) {
|
|
std::transform(timecodes.begin(), timecodes.end(), timecodes.begin(), std::bind2nd(std::minus<int>(), front));
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
normalize_timecodes(this->timecodes);
|
|
fps = (timecodes.size() - 1) * 1000. / timecodes.back();
|
|
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);
|
|
normalize_timecodes(timecodes);
|
|
fps = (timecodes.size() - 1) * 1000. / timecodes.back();
|
|
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 = (int)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 - 1]
|
|
// START: [1 - X , 0]
|
|
// END: [1, X]
|
|
|
|
// 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 < 0) {
|
|
return (int)floor(ms * fps / 1000.);
|
|
}
|
|
if (ms > timecodes.back()) {
|
|
return round((ms - timecodes.back()) * fps / 1000.) + (int)timecodes.size() - 1;
|
|
}
|
|
|
|
return (int)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.);
|
|
}
|
|
if (frame >= (signed)timecodes.size()) {
|
|
return round((frame - timecodes.size() + 1) * 1000. / fps + last);
|
|
}
|
|
return timecodes[frame];
|
|
}
|
|
|
|
}
|
|
}
|