359 lines
10 KiB
C++
359 lines
10 KiB
C++
/***
|
|
|
|
Olive - Non-Linear Video Editor
|
|
Copyright (C) 2022 Olive Team
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
***/
|
|
|
|
#include "timecodefunctions.h"
|
|
|
|
extern "C" {
|
|
#include <libavutil/mathematics.h>
|
|
}
|
|
|
|
#include <QRegularExpression>
|
|
#include <QtMath>
|
|
|
|
#include "config/config.h"
|
|
|
|
namespace olive {
|
|
|
|
QString padded(int64_t arg, int padding) {
|
|
return QStringLiteral("%1").arg(arg, padding, 10, QChar('0'));
|
|
}
|
|
|
|
QString Timecode::time_to_timecode(const rational &time, const rational &timebase, const Timecode::Display &display, bool show_plus_if_positive)
|
|
{
|
|
if (timebase.isNull()) {
|
|
return QStringLiteral("INVALID TIMEBASE");
|
|
}
|
|
|
|
double time_dbl = time.toDouble();
|
|
|
|
switch (display) {
|
|
case kTimecodeNonDropFrame:
|
|
case kTimecodeDropFrame:
|
|
case kTimecodeSeconds:
|
|
{
|
|
QString prefix;
|
|
|
|
if (time_dbl < 0) {
|
|
prefix = "-";
|
|
} else if (show_plus_if_positive) {
|
|
prefix = "+";
|
|
}
|
|
|
|
if (display == kTimecodeSeconds) {
|
|
time_dbl = qAbs(time_dbl);
|
|
|
|
int64_t total_seconds = qFloor(time_dbl);
|
|
|
|
int64_t hours = total_seconds / 3600;
|
|
int64_t mins = total_seconds / 60 - hours * 60;
|
|
int64_t secs = total_seconds - mins * 60;
|
|
int64_t fraction = qRound64((time_dbl - static_cast<double>(total_seconds)) * 1000);
|
|
|
|
return QStringLiteral("%1%2:%3:%4.%5").arg(prefix,
|
|
padded(hours, 2),
|
|
padded(mins, 2),
|
|
padded(secs, 2),
|
|
padded(fraction, 3));
|
|
} else {
|
|
// Determine what symbol to separate frames (";" is used for drop frame, ":" is non-drop frame)
|
|
QString frame_token;
|
|
double frame_rate = timebase.flipped().toDouble();
|
|
int rounded_frame_rate = qRound(frame_rate);
|
|
int64_t frames, secs, mins, hours;
|
|
int64_t f = qAbs(time_to_timestamp(time, timebase));
|
|
|
|
if (display == kTimecodeDropFrame && TimebaseIsDropFrame(timebase)) {
|
|
frame_token = ";";
|
|
|
|
/**
|
|
* CONVERT A FRAME NUMBER TO DROP FRAME TIMECODE
|
|
*
|
|
* Code by David Heidelberger, adapted from Andrew Duncan, further adapted for Olive by Olive Team
|
|
* Given an int called framenumber and a double called framerate
|
|
* Framerate should be 29.97, 59.94, or 23.976, otherwise the calculations will be off.
|
|
*/
|
|
|
|
// If frame number is greater than 24 hrs, next operation will rollover clock
|
|
f %= (qRound(frame_rate*3600)*24);
|
|
|
|
// Number of frames per ten minutes
|
|
int64_t framesPer10Minutes = qRound(frame_rate * 600);
|
|
int64_t d = f / framesPer10Minutes;
|
|
int64_t m = f % framesPer10Minutes;
|
|
|
|
// Number of frames to drop on the minute marks is the nearest integer to 6% of the framerate
|
|
int64_t dropFrames = qRound(frame_rate * (2.0/30.0));
|
|
|
|
// Number of frames per minute is the round of the framerate * 60 minus the number of dropped frames
|
|
f += dropFrames*9*d;
|
|
if (m > dropFrames) {
|
|
f += dropFrames * ((m - dropFrames) / (qRound(frame_rate)*60 - dropFrames));
|
|
}
|
|
} else {
|
|
frame_token = ":";
|
|
}
|
|
|
|
// non-drop timecode
|
|
hours = f / (3600*rounded_frame_rate);
|
|
mins = f / (60*rounded_frame_rate) % 60;
|
|
secs = f / rounded_frame_rate % 60;
|
|
frames = f % rounded_frame_rate;
|
|
|
|
return QStringLiteral("%1%2:%3:%4%5%6").arg(prefix,
|
|
padded(hours, 2),
|
|
padded(mins, 2),
|
|
padded(secs, 2),
|
|
frame_token,
|
|
padded(frames, 2));
|
|
}
|
|
}
|
|
case kFrames:
|
|
return QString::number(time_to_timestamp(time, timebase));
|
|
case kMilliseconds:
|
|
return QString::number(qRound(time_dbl * 1000));
|
|
}
|
|
|
|
return QStringLiteral("INVALID TIMECODE MODE");
|
|
}
|
|
|
|
int64_t StrToInt64EmptyTolerant(const QString &s, bool *ok)
|
|
{
|
|
if (s.isEmpty()) {
|
|
if (ok) *ok = true;
|
|
return 0;
|
|
} else {
|
|
return s.toLongLong(ok);
|
|
}
|
|
}
|
|
|
|
double StrToDoubleEmptyTolerant(const QString &s, bool *ok)
|
|
{
|
|
if (s.isEmpty()) {
|
|
if (ok) *ok = true;
|
|
return 0;
|
|
} else {
|
|
return s.toDouble(ok);
|
|
}
|
|
}
|
|
|
|
rational Timecode::timecode_to_time(const QString &timecode, const rational &timebase, const Timecode::Display &display, bool *ok)
|
|
{
|
|
if (timecode.isEmpty()) {
|
|
goto err_fatal;
|
|
}
|
|
|
|
switch (display) {
|
|
case kTimecodeNonDropFrame:
|
|
case kTimecodeDropFrame:
|
|
case kTimecodeSeconds:
|
|
{
|
|
QStringList timecode_split = timecode.split(QRegularExpression("(:)|(;)"));
|
|
|
|
const int element_count = display == kTimecodeSeconds ? 3 : 4;
|
|
|
|
// Remove excess tokens (we're only interested in HH:MM:SS.FF)
|
|
while (timecode_split.size() > element_count) {
|
|
timecode_split.removeLast();
|
|
}
|
|
|
|
// For easier index calculations, ensure minimum size
|
|
while (timecode_split.size() < element_count) {
|
|
timecode_split.prepend(QString());
|
|
}
|
|
|
|
bool negative = timecode.trimmed().startsWith('-');
|
|
|
|
double frame_rate = timebase.flipped().toDouble();
|
|
int rounded_frame_rate = qRound(frame_rate);
|
|
|
|
bool valid;
|
|
rational time;
|
|
|
|
int64_t hours = StrToInt64EmptyTolerant(timecode_split.at(0), &valid);
|
|
if (!valid) goto err_fatal;
|
|
int64_t mins = StrToInt64EmptyTolerant(timecode_split.at(1), &valid);
|
|
if (!valid) goto err_fatal;
|
|
|
|
if (display == kTimecodeSeconds) {
|
|
double secs = StrToDoubleEmptyTolerant(timecode_split.at(2), &valid);
|
|
if (!valid) goto err_fatal;
|
|
|
|
time = rational::fromDouble(hours * 3600 + mins * 60 + secs);
|
|
} else {
|
|
int64_t secs = StrToInt64EmptyTolerant(timecode_split.at(2), &valid);
|
|
if (!valid) goto err_fatal;
|
|
int64_t frames = StrToInt64EmptyTolerant(timecode_split.at(3), &valid);
|
|
if (!valid) goto err_fatal;
|
|
|
|
int64_t sec_count = (hours*3600 + mins*60 + secs);
|
|
int64_t frame_count = sec_count*rounded_frame_rate + frames;
|
|
|
|
if (display == kTimecodeDropFrame && TimebaseIsDropFrame(timebase)) {
|
|
|
|
// Number of frames to drop on the minute marks is the nearest integer to 6% of the framerate
|
|
int64_t dropFrames = qRound64(frame_rate * (2.0/30.0));
|
|
|
|
// d and m need to be calculated from
|
|
int64_t real_fr_ts = qRound64(static_cast<double>(sec_count)*frame_rate) + frames;
|
|
|
|
int64_t framesPer10Minutes = qRound(frame_rate * 600);
|
|
int64_t d = real_fr_ts / framesPer10Minutes;
|
|
int64_t m = real_fr_ts % framesPer10Minutes;
|
|
|
|
if (m > dropFrames) {
|
|
frame_count -= dropFrames * ((m - dropFrames) / (qRound(frame_rate)*60 - dropFrames));
|
|
}
|
|
frame_count -= dropFrames*9*d;
|
|
}
|
|
|
|
time = timestamp_to_time(frame_count, timebase);
|
|
}
|
|
|
|
if (ok) *ok = true;
|
|
|
|
if (negative) time = -time;
|
|
|
|
return time;
|
|
}
|
|
case kMilliseconds:
|
|
{
|
|
bool valid;
|
|
double timecode_secs = timecode.toDouble(&valid);
|
|
|
|
if (valid) {
|
|
// Convert milliseconds to seconds
|
|
timecode_secs *= 0.001;
|
|
|
|
// Convert seconds to rational
|
|
return rational::fromDouble(timecode_secs, ok);
|
|
} else {
|
|
goto err_fatal;
|
|
}
|
|
}
|
|
case kFrames:
|
|
{
|
|
bool valid;
|
|
int64_t ts = timecode.toLongLong(&valid);
|
|
if (!valid) {
|
|
goto err_fatal;
|
|
}
|
|
|
|
if (ok) *ok = true;
|
|
return timestamp_to_time(ts, timebase);
|
|
}
|
|
}
|
|
|
|
err_fatal:
|
|
if (ok) *ok = false;
|
|
return 0;
|
|
}
|
|
|
|
rational Timecode::snap_time_to_timebase(const rational &time, const rational &timebase, Rounding floor)
|
|
{
|
|
// Just convert to a timestamp in timebase units and back
|
|
int64_t timestamp = time_to_timestamp(time, timebase, floor);
|
|
|
|
return timestamp_to_time(timestamp, timebase);
|
|
}
|
|
|
|
rational Timecode::timestamp_to_time(const int64_t ×tamp, const rational &timebase)
|
|
{
|
|
int64_t num = int64_t(timebase.numerator()) * timestamp;
|
|
int64_t den = timebase.denominator();
|
|
|
|
int num_r, den_r;
|
|
|
|
av_reduce(&num_r, &den_r, num, den, INT_MAX);
|
|
|
|
return rational(num_r, den_r);
|
|
}
|
|
|
|
bool Timecode::TimebaseIsDropFrame(const rational &timebase)
|
|
{
|
|
return (timebase.numerator() != 1);
|
|
}
|
|
|
|
QString Timecode::TimeToString(int64_t ms)
|
|
{
|
|
int64_t total_seconds = ms / 1000;
|
|
int64_t ss = total_seconds % 60;
|
|
int64_t mm = (total_seconds / 60) % 60;
|
|
int64_t hh = total_seconds / 3600;
|
|
|
|
return QStringLiteral("%1:%2:%3")
|
|
.arg(hh, 2, 10, QChar('0'))
|
|
.arg(mm, 2, 10, QChar('0'))
|
|
.arg(ss, 2, 10, QChar('0'));
|
|
}
|
|
|
|
int64_t Timecode::time_to_timestamp(const rational &time, const rational &timebase, Rounding floor)
|
|
{
|
|
return time_to_timestamp(time.toDouble(), timebase, floor);
|
|
}
|
|
|
|
int64_t Timecode::time_to_timestamp(const double &time, const rational &timebase, Rounding floor)
|
|
{
|
|
const double d = time * timebase.flipped().toDouble();
|
|
|
|
if (std::isnan(d)) {
|
|
return 0;
|
|
}
|
|
|
|
const double eps = 0.000000000001;
|
|
|
|
switch (floor) {
|
|
case kRound:
|
|
default:
|
|
return qRound64(d);
|
|
case kFloor:
|
|
if (d > qCeil(d)-eps) {
|
|
return qCeil(d);
|
|
} else {
|
|
return qFloor(d);
|
|
}
|
|
case kCeil:
|
|
if (d < qFloor(d)+eps) {
|
|
return qFloor(d);
|
|
} else {
|
|
return qCeil(d);
|
|
}
|
|
}
|
|
}
|
|
|
|
int64_t Timecode::rescale_timestamp(const int64_t &ts, const rational &source, const rational &dest)
|
|
{
|
|
if (source == dest) {
|
|
return ts;
|
|
}
|
|
|
|
return av_rescale_q(ts, source.toAVRational(), dest.toAVRational());
|
|
}
|
|
|
|
int64_t Timecode::rescale_timestamp_ceil(const int64_t &ts, const rational &source, const rational &dest)
|
|
{
|
|
if (source == dest) {
|
|
return ts;
|
|
}
|
|
|
|
return av_rescale_q_rnd(ts, source.toAVRational(), dest.toAVRational(), AV_ROUND_UP);
|
|
}
|
|
|
|
}
|