- Reworked the SMPTE timecode handling with Plorkyeran's help. It does now handle dropframe timecodes as well; the ms->SMPTE handling has been tested and seems reasonably correct, while the reverse conversion remains untested and unused. The Adobe Encore export filter will now use dropframe timecodes properly (previously it would play pretend with wallclock hours/minutes/seconds and incorrect frame numbers).

- Changed the SubtitleFormat::AskForFPS dialog box; removed the "PAL/NTSC only" choice and added a "show SMPTE dropframe" parameter instead. Also added 50fps as a choice.

- While I was at it, reworked the TranStation export filter so it actually looks ahead to see if the next line will overlap with the current, and if so, move the end time of the current line backwards one frame, which fixes #767

Originally committed to SVN as r2920.
This commit is contained in:
Karl Blomster 2009-05-13 20:24:21 +00:00
parent 7215f354b9
commit ffa5a2021d
8 changed files with 255 additions and 110 deletions

View file

@ -39,6 +39,7 @@
#include "config.h"
#include <wx/regex.h>
#include <math.h>
#include <fstream>
#include <algorithm>
#include "ass_time.h"
@ -288,9 +289,18 @@ int AssTime::GetTimeCentiseconds() { return (time % 1000)/10; }
FractionalTime::FractionalTime (wxString separator, double numerator, double denominator) {
///////
// Constructor
FractionalTime::FractionalTime (wxString separator, int numerator, int denominator, bool dropframe) {
drop = dropframe;
if (drop) {
// no dropframe for any other framerates
num = 30000;
den = 1001;
} else {
num = numerator;
den = denominator;
}
sep = separator;
// fractions < 1 are not welcome here
@ -300,20 +310,23 @@ FractionalTime::FractionalTime (wxString separator, double numerator, double den
throw _T("FractionalTime: no separator specified");
}
///////
// Destructor
FractionalTime::~FractionalTime () {
sep.Clear();
}
int64_t FractionalTime::ToMillisecs (wxString _text) {
///////
// SMPTE text string to milliseconds conversion
int FractionalTime::ToMillisecs (wxString _text) {
wxString text = _text;
wxString re_str = _T("");
wxString sep_e = _T("\\") + sep; // escape this just in case it may be a reserved regex character
text.Trim(false);
text.Trim(true);
long h=0,m=0,s=0,ms=0,f=0;
// hour minute second fraction
re_str << _T("(\\d+)") << sep_e << _T("(\\d+)") << sep_e << _T("(\\d+)") << sep_e << _T("(\\d+)");
re_str << _T("(\\d+)") << sep << _T("(\\d+)") << sep << _T("(\\d+)") << sep << _T("(\\d+)");
wxRegEx re(re_str, wxRE_ADVANCED);
if (!re.IsValid())
@ -321,32 +334,106 @@ int64_t FractionalTime::ToMillisecs (wxString _text) {
if (!re.Matches(text))
return 0; // FIXME: throw here too?
re.GetMatch(text, 1).ToLong(&h);
re.GetMatch(text, 2).ToLong(&m);
re.GetMatch(text, 3).ToLong(&s);
re.GetMatch(text, 4).ToLong(&f);
// FIXME: find out how to do this in a sane way
//if ((double)f >= ((double)num/(double)den) // overflow?
// f = (num/den - 1);
ms = long((1000.0 / (num/den)) * (double)f);
re.GetMatch(text,1).ToLong(&h);
re.GetMatch(text,2).ToLong(&m);
re.GetMatch(text,3).ToLong(&s);
re.GetMatch(text,4).ToLong(&f);
return (int64_t)((h * 3600000) + (m * 60000) + (s * 1000) + ms);
int msecs_f = 0;
int fn = 0;
// dropframe? do silly things
if (drop) {
fn += h * frames_per_period * 6;
fn += (m % 10) * frames_per_period;
if (m > 0) {
fn += 1800;
m--;
fn += m * 1798; // two timestamps dropped per minute after the first
fn += s * 30 + f - 2;
}
else { // minute is evenly divisible by 10, keep first two timestamps
fn += s * 30;
fn += f;
}
msecs_f = (fn * num) / den;
}
// no dropframe, may or may not sync with wallclock time
// (see comment in FromMillisecs for an explanation of why it's done like this)
else {
int fps_approx = floor((double(num)/double(den))+0.5);
fn += h * 3600 * fps_approx;
fn += m * 60 * fps_approx;
fn += s * fps_approx;
fn += f;
msecs_f = (fn * num) / den;
}
return msecs_f;
}
///////
// SMPTE text string to AssTime conversion
AssTime FractionalTime::ToAssTime (wxString _text) {
AssTime time;
time.SetMS((int)ToMillisecs(_text));
return time;
}
///////
// AssTime to SMPTE text string conversion
wxString FractionalTime::FromAssTime(AssTime time) {
return FromMillisecs((int64_t)time.GetMS());
return FromMillisecs(time.GetMS());
}
///////
// Milliseconds to SMPTE text string conversion
wxString FractionalTime::FromMillisecs(int64_t msec) {
int h = msec / 3600000;
int m = (msec % 3600000)/60000;
int s = (msec % 60000)/1000;
int f = int((msec % 1000) * ((num/den) / 1000.0));
int h=0, m=0, s=0, f=0; // hours, minutes, seconds, fractions
int fn = (msec*(int64_t)num) / (1000*den); // frame number
// dropframe?
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((double(num)/double(den))+0.5);
int frames_per_h = 3600*fps_approx;
int frames_per_m = 60*fps_approx;
int frames_per_s = fps_approx;
while (fn >= frames_per_h) {
h++; fn -= frames_per_h;
}
while (fn >= frames_per_m) {
m++; fn -= frames_per_m;
}
while (fn >= frames_per_s) {
s++; fn -= frames_per_s;
}
f = fn;
}
return wxString::Format(_T("%02i") + sep + _T("%02i") + sep + _T("%02i") + sep + _T("%02i"),h,m,s,f);
}

View file

@ -78,21 +78,28 @@ bool operator <= (AssTime &t1, AssTime &t2);
bool operator >= (AssTime &t1, AssTime &t2);
/////////////////////////////
// Class for that annoying SMPTE format timecodes stuff
class FractionalTime {
private:
int64_t time; // milliseconds, like in AssTime
double num, den; // numerator/denominator
int time; // milliseconds, like in AssTime
int num, den; // numerator/denominator
bool drop; // EVIL
wxString sep; // separator; someone might have separators of more than one character :V
// A period is roughly 10 minutes and is used for the dropframe stuff;
// SMPTE dropframe timecodes drops 18 timestamps per 18000, hence the number 17982.
static const int frames_per_period = 17982;
public:
// dumb assumption? I give no fuck
FractionalTime(wxString separator, double numerator=30.0, double denominator=1.0);
// NOTE: separator can be a regex! at least if you only plan on doing SMPTE->somethingelse.
FractionalTime(wxString separator, int numerator=30, int denominator=1, bool dropframe=false);
~FractionalTime();
AssTime ToAssTime(wxString fractime);
int64_t ToMillisecs(wxString fractime);
int ToMillisecs(wxString fractime);
wxString FromAssTime(AssTime time);
wxString FromMillisecs(int64_t msec);

View file

@ -279,11 +279,13 @@ wxString SubtitleFormat::GetWildcards(int mode) {
/////////////////////////////////
// Ask the user to enter the FPS
double SubtitleFormat::AskForFPS(bool palNtscOnly) {
SubtitleFormat::FPSRational SubtitleFormat::AskForFPS(bool showSMPTE) {
wxArrayString choices;
FPSRational fps_rat;
fps_rat.smpte_dropframe = false; // ensure it's false by default
// Video FPS
bool vidLoaded = !palNtscOnly && VFR_Output.IsLoaded();
bool vidLoaded = VFR_Output.IsLoaded();
if (vidLoaded) {
wxString vidFPS;
if (VFR_Output.GetFrameRateType() == VFR) vidFPS = _T("VFR");
@ -292,49 +294,73 @@ double SubtitleFormat::AskForFPS(bool palNtscOnly) {
}
// Standard FPS values
if (!palNtscOnly) {
choices.Add(_("15.000 FPS"));
choices.Add(_("23.976 FPS (Decimated NTSC)"));
choices.Add(_("24.000 FPS (FILM)"));
}
choices.Add(_("25.000 FPS (PAL)"));
choices.Add(_("29.970 FPS (NTSC)"));
if (!palNtscOnly) {
if (showSMPTE)
choices.Add(_("29.970 FPS (NTSC with SMPTE dropframe)"));
choices.Add(_("30.000 FPS"));
choices.Add(_("50.000 FPS (PAL x2)"));
choices.Add(_("59.940 FPS (NTSC x2)"));
choices.Add(_("60.000 FPS"));
choices.Add(_("119.880 FPS (NTSC x4)"));
choices.Add(_("120.000 FPS"));
}
// Ask
int choice = wxGetSingleChoiceIndex(_("Please choose the appropriate FPS for the subtitles:"),_("FPS"),choices);
if (choice == -1) return 0.0;
if (choice == -1) {
fps_rat.num = 0;
fps_rat.den = 0;
// PAL/NTSC choice
if (palNtscOnly) {
if (choice == 0) return 25.0;
else return 30.0 / 1.001;
return fps_rat;
}
// Get FPS from choice
if (vidLoaded) choice--;
// dropframe was displayed, that means all choices >4 are bumped up by 1
if (showSMPTE) {
switch (choice) {
case -1: return -1.0; break; // VIDEO
case 0: return 15.0; break;
case 1: return 24.0 / 1.001; break;
case 2: return 24.0; break;
case 3: return 25.0; break;
case 4: return 30.0 / 1.001; break;
case 5: return 30.0; break;
case 6: return 60.0 / 1.001; break;
case 7: return 60.0; break;
case 8: return 120.0 / 1.001; break;
case 9: return 120.0; break;
case -1: fps_rat.num = -1; fps_rat.den = 1; break; // VIDEO
case 0: fps_rat.num = 15; fps_rat.den = 1; break;
case 1: fps_rat.num = 24000; fps_rat.den = 1001; break;
case 2: fps_rat.num = 24; fps_rat.den = 1; break;
case 3: fps_rat.num = 25; fps_rat.den = 1; break;
case 4: fps_rat.num = 30000; fps_rat.den = 1001; break;
case 5: fps_rat.num = 30000; fps_rat.den = 1001; fps_rat.smpte_dropframe = true; break;
case 6: fps_rat.num = 30; fps_rat.den = 1; break;
case 7: fps_rat.num = 50; fps_rat.den = 1; break;
case 8: fps_rat.num = 60000; fps_rat.den = 1001; break;
case 9: fps_rat.num = 60; fps_rat.den = 1; break;
case 10: fps_rat.num = 120000; fps_rat.den = 1001; break;
case 11: fps_rat.num = 120; fps_rat.den = 1; break;
}
return fps_rat;
} else {
// dropframe wasn't displayed
switch (choice) {
case -1: fps_rat.num = -1; fps_rat.den = 1; break; // VIDEO
case 0: fps_rat.num = 15; fps_rat.den = 1; break;
case 1: fps_rat.num = 24000; fps_rat.den = 1001; break;
case 2: fps_rat.num = 24; fps_rat.den = 1; break;
case 3: fps_rat.num = 25; fps_rat.den = 1; break;
case 4: fps_rat.num = 30000; fps_rat.den = 1001; break;
case 5: fps_rat.num = 30; fps_rat.den = 1; break;
case 6: fps_rat.num = 50; fps_rat.den = 1; break;
case 7: fps_rat.num = 60000; fps_rat.den = 1001; break;
case 8: fps_rat.num = 60; fps_rat.den = 1; break;
case 9: fps_rat.num = 120000; fps_rat.den = 1001; break;
case 10: fps_rat.num = 120; fps_rat.den = 1; break;
}
return fps_rat;
}
// fubar
return 0.0;
fps_rat.num = 0;
fps_rat.den = 0;
return fps_rat;
}

View file

@ -65,6 +65,12 @@ private:
static bool loaded;
protected:
struct FPSRational {
int num;
int den;
bool smpte_dropframe;
};
std::list<AssEntry*> *Line;
void CreateCopy();
@ -81,7 +87,7 @@ protected:
void LoadDefault(bool defline=true);
AssFile *GetAssFile() { return assFile; }
int AddLine(wxString data,wxString group,int lasttime,int &version,wxString *outgroup=NULL);
double AskForFPS(bool palNtscOnly=false);
FPSRational AskForFPS(bool showSMPTE=false);
virtual wxString GetName()=0;
virtual wxArrayString GetReadWildcards();

View file

@ -70,8 +70,8 @@ bool EncoreSubtitleFormat::CanWriteFile(wxString filename) {
// Write file
void EncoreSubtitleFormat::WriteFile(wxString _filename,wxString encoding) {
// Get FPS
double fps = AskForFPS(true);
if (fps <= 0.0) return;
FPSRational fps_rat = AskForFPS(true);
if (fps_rat.num <= 0 || fps_rat.den <= 0) return;
// Open file
TextFileWriter file(_filename,encoding);
@ -88,14 +88,14 @@ void EncoreSubtitleFormat::WriteFile(wxString _filename,wxString encoding) {
using std::list;
int i = 0;
// Encore wants ; instead of : if we're dealing with NTSC
FractionalTime fp(fps > 26.0 ? _T(";") : _T(":"), fps);
// Encore wants ; instead of : if we're dealing with NTSC dropframe stuff
FractionalTime ft(fps_rat.smpte_dropframe ? _T(";") : _T(":"), fps_rat.num, fps_rat.den, fps_rat.smpte_dropframe);
for (list<AssEntry*>::iterator cur=Line->begin();cur!=Line->end();cur++) {
AssDialogue *current = AssEntry::GetAsDialogue(*cur);
if (current && !current->Comment) {
// Time stamps
wxString timeStamps = wxString::Format(_T("%i "),++i) + fp.FromAssTime(current->Start) + _T(" ") + fp.FromAssTime(current->End);
wxString timeStamps = wxString::Format(_T("%i "),++i) + ft.FromAssTime(current->Start) + _T(" ") + ft.FromAssTime(current->End);
// Write
file.WriteLineToFile(timeStamps + current->Text);

View file

@ -110,6 +110,7 @@ void MicroDVDSubtitleFormat::ReadFile(wxString filename,wxString forceEncoding)
// Loop
bool isFirst = true;
FPSRational fps_rat;
double fps = 0.0;
while (file.HasMoreLines()) {
wxString line = file.ReadLineFromFile();
@ -133,9 +134,9 @@ void MicroDVDSubtitleFormat::ReadFile(wxString filename,wxString forceEncoding)
// If it wasn't an fps line, ask the user for it
if (fps <= 0.0) {
fps = AskForFPS();
if (fps == 0.0) return;
else if (fps > 0.0) cfr.SetCFR(fps);
fps_rat = AskForFPS();
if (fps_rat.num == 0) return;
else if (fps_rat.num > 0) cfr.SetCFR(double(fps_rat.num)/double(fps_rat.den));
else rate = &VFR_Output;
}
else {
@ -172,9 +173,10 @@ void MicroDVDSubtitleFormat::WriteFile(wxString filename,wxString encoding) {
// Set FPS
FrameRate cfr;
FrameRate *rate = &cfr;
double fps = AskForFPS();
if (fps == 0.0) return;
else if (fps > 0.0) cfr.SetCFR(fps);
FPSRational fps_rat = AskForFPS();
if (fps_rat.num == 0 || fps_rat.den == 0) return;
double fps = double(fps_rat.num) / double(fps_rat.den);
if (fps > 0.0) cfr.SetCFR(fps);
else rate = &VFR_Output;
// Convert file

View file

@ -74,8 +74,8 @@ bool TranStationSubtitleFormat::CanWriteFile(wxString filename) {
// Write file
void TranStationSubtitleFormat::WriteFile(wxString _filename,wxString encoding) {
// Get FPS
double fps = AskForFPS(true);
if (fps <= 0.0) return;
FPSRational fps_rat = AskForFPS(true);
if (fps_rat.num <= 0 || fps_rat.den <= 0) return;
// Open file
TextFileWriter file(_filename,encoding);
@ -89,9 +89,31 @@ void TranStationSubtitleFormat::WriteFile(wxString _filename,wxString encoding)
// Write lines
using std::list;
AssDialogue *current = NULL;
AssDialogue *next = NULL;
for (list<AssEntry*>::iterator cur=Line->begin();cur!=Line->end();cur++) {
AssDialogue *current = AssEntry::GetAsDialogue(*cur);
if (next)
current = next;
next = AssEntry::GetAsDialogue(*cur);
if (current && !current->Comment) {
// Write text
file.WriteLineToFile(ConvertLine(current,&fps_rat,(next && !next->Comment) ? next->Start.GetMS() : -1));
file.WriteLineToFile(_T(""));
}
}
// flush last line
if (next && !next->Comment)
file.WriteLineToFile(ConvertLine(next,&fps_rat,-1));
// Every file must end with this line
file.WriteLineToFile(_T("SUB["));
// Clean up
ClearCopy();
}
wxString TranStationSubtitleFormat::ConvertLine(AssDialogue *current, FPSRational *fps_rat, int nextl_start) {
// Get line data
AssStyle *style = GetAssFile()->GetStyle(current->Style);
int valign = 0;
@ -112,13 +134,15 @@ void TranStationSubtitleFormat::WriteFile(wxString _filename,wxString encoding)
// Write header
AssTime start = current->Start;
AssTime end = current->End;
// Subtract half a frame duration from end time, since it is inclusive
// and we otherwise run the risk of having two lines overlap in a
// frame, when they should run right into each other.
end.SetMS(end.GetMS() - (int)(500.0/fps));
FractionalTime ft(_T(":"),fps);
wxString header = wxString::Format(_T("SUB[%i%s%s "),valign,halign,type) + ft.FromAssTime(start) + _T(">") + ft.FromAssTime(end) + _T("]");
file.WriteLineToFile(header);
// Subtract one frame if the end time of the current line is equal to the
// start of next one, since the end timestamp is inclusive and the lines
// would overlap if left as is.
if (nextl_start > 0 && end.GetMS() == nextl_start)
end.SetMS(end.GetMS() - ((1000*fps_rat->den)/fps_rat->num));
FractionalTime ft(_T(":"), fps_rat->num, fps_rat->den, fps_rat->smpte_dropframe);
wxString header = wxString::Format(_T("SUB[%i%s%s "),valign,halign,type) + ft.FromAssTime(start) + _T(">") + ft.FromAssTime(end) + _T("]\r\n");
// Process text
wxString lineEnd = _T("\r\n");
@ -128,15 +152,5 @@ void TranStationSubtitleFormat::WriteFile(wxString _filename,wxString encoding)
current->Text.Replace(_T("\\N"),lineEnd,true);
while (current->Text.Replace(lineEnd+lineEnd,lineEnd,true));
// Write text
file.WriteLineToFile(current->Text);
file.WriteLineToFile(_T(""));
}
}
// Every file must end with this line
file.WriteLineToFile(_T("SUB["));
// Clean up
ClearCopy();
return header + current->Text;
}

View file

@ -45,6 +45,9 @@
//////////////////////
// TranStation writer
class TranStationSubtitleFormat : public SubtitleFormat {
private:
wxString ConvertLine(AssDialogue *line, FPSRational *fps_rat, int nextl_start);
public:
wxString GetName();
wxArrayString GetWriteWildcards();