1+ #include " VapourSynth.h"
2+ #include " VSHelper.h"
3+ #include < vector>
4+ #include < deque>
5+ #include < fstream>
6+ #include < string>
7+ #define EPSILON 0.0005
8+
9+ struct timestamp {
10+ unsigned int num;
11+ double start;
12+ double end;
13+ };
14+
15+ struct VFRToCFRData {
16+ VSNodeRef *node;
17+ VSVideoInfo vi;
18+ bool drop;
19+ std::vector<unsigned int > frameMap;
20+ };
21+
22+ static void VS_CC VFRToCFRInit (VSMap *in, VSMap *out, void **instanceData, VSNode *node, VSCore *core, const VSAPI *vsapi) {
23+ VFRToCFRData *d = static_cast <VFRToCFRData *>(*instanceData);
24+ vsapi->setVideoInfo (&d->vi , 1 , node);
25+ }
26+
27+ static const VSFrameRef *VS_CC VFRToCFRGetFrame (int n, int activationReason, void **instanceData, void **frameData, VSFrameContext *frameCtx, VSCore *core, const VSAPI *vsapi) {
28+ VFRToCFRData *d = static_cast <VFRToCFRData *>(* instanceData);
29+
30+ if (activationReason == arInitial) {
31+ vsapi->requestFrameFilter (d->frameMap [n], d->node , frameCtx);
32+ }
33+ else if (activationReason == arAllFramesReady) {
34+ const VSFrameRef *src = vsapi->getFrameFilter (d->frameMap [n], d->node , frameCtx);
35+ VSFrameRef *dst = vsapi->copyFrame (src, core);
36+ VSMap *m = vsapi->getFramePropsRW (dst);
37+ vsapi->freeFrame (src);
38+ vsapi->propSetInt (m, " _DurationNum" , d->vi .fpsDen , paReplace);
39+ vsapi->propSetInt (m, " _DurationDen" , d->vi .fpsNum , paReplace);
40+ return dst;
41+ }
42+
43+ return nullptr ;
44+ }
45+
46+ static void VS_CC VFRToCFRFree (void *instanceData, VSCore *core, const VSAPI *vsapi) {
47+ VFRToCFRData *d = static_cast <VFRToCFRData *>(instanceData);
48+ vsapi->freeNode (d->node );
49+ delete d;
50+ }
51+
52+ static void VS_CC VFRToCFRCreate (const VSMap *in, VSMap *out, void *userData, VSCore *core, const VSAPI *vsapi) {
53+ VFRToCFRData d{ vsapi->propGetNode (in, " clip" , 0 , 0 ), *vsapi->getVideoInfo (d.node ) };
54+ int err;
55+
56+ d.vi .fpsNum = vsapi->propGetInt (in, " fpsnum" , 0 , &err);
57+ d.vi .fpsDen = vsapi->propGetInt (in, " fpsden" , 0 , &err);
58+ if (d.vi .fpsNum <= 0 || d.vi .fpsDen <= 0 ) {
59+ vsapi->setError (out, " Both fpsnum and fpsden need to be greater than 0." );
60+ vsapi->freeNode (d.node );
61+ return ;
62+ }
63+
64+ d.drop = !!vsapi->propGetInt (in, " drop" , 0 , &err);
65+ if (err)
66+ d.drop = true ;
67+
68+ std::string timecodes{ vsapi->propGetData (in, " timecodes" , 0 , &err) };
69+
70+ unsigned int count{ 0 };
71+ std::deque<timestamp> inTimes;
72+ std::ifstream file (timecodes);
73+ if (!file) {
74+ vsapi->setError (out, " Failed to open the timecodes file." );
75+ vsapi->freeNode (d.node );
76+ return ;
77+ }
78+ std::string line;
79+ std::getline (file, line);
80+ while (file) {
81+ double i = 0 ;
82+ file >> i;
83+ timestamp time;
84+ time.start = i / 1000.0 ;
85+ time.num = count;
86+ count++;
87+ inTimes.push_back (time);
88+ }
89+
90+ // If last line is NULL, remove the last timestamp.
91+ if (inTimes.size () > 1 && inTimes.back ().start <= 0 )
92+ inTimes.pop_back ();
93+
94+ for (unsigned int i = 0 ; i < inTimes.size (); i++) {
95+ if (i != inTimes.size () - 1 ) {
96+ inTimes[i].end = inTimes[i + 1 ].start ;
97+ }
98+ }
99+ inTimes.back ().end = inTimes.back ().start + (inTimes.back ().start / (inTimes.size () - 1 ));
100+
101+ unsigned int frames = (inTimes.back ().start * d.vi .fpsNum / d.vi .fpsDen );
102+
103+ double check = frames / static_cast <double >(d.vi .fpsNum ) * d.vi .fpsDen ;
104+ while (check < inTimes.back ().end && abs (check - inTimes.back ().end ) > static_cast <double >(EPSILON)) {
105+ frames++;
106+ check = frames / static_cast <double >(d.vi .fpsNum ) * d.vi .fpsDen ;
107+ }
108+
109+ d.frameMap .resize (frames);
110+ bool drop = false ;
111+ std::deque<timestamp> choices;
112+ for (unsigned int i = 0 ; i < frames; i++) {
113+ double starttime = i * d.vi .fpsDen / static_cast <double >(d.vi .fpsNum );
114+ double endtime = (i + 1 ) * d.vi .fpsDen / static_cast <double >(d.vi .fpsNum );
115+
116+ // Remove frames that are eariler than the last frame chosen.
117+ while (!choices.empty () && d.frameMap [i - 1 ] > choices.front ().num ) {
118+ choices.pop_front ();
119+ }
120+
121+ // Remove frames than can't be a choice for this frame.
122+ while (!choices.empty () && (starttime > choices.front ().end || abs (starttime - choices.front ().end ) < static_cast <double >(EPSILON))) {
123+ choices.pop_front ();
124+ }
125+
126+ // Add possible choices.
127+ while (!inTimes.empty () && endtime > inTimes.front ().start && abs (endtime - inTimes.front ().start ) > static_cast <double >(EPSILON)) {
128+
129+ // This if statement can only be false when the framerate from the timecodes
130+ // file is really low (unrealistic).
131+ if (starttime < inTimes.front ().end && abs (starttime - inTimes.front ().end ) > static_cast <double >(EPSILON)) {
132+ choices.push_back (inTimes.front ());
133+ }
134+ inTimes.pop_front ();
135+ }
136+
137+ // Should only occur with timestamps not starting at 0.
138+ if (choices.size () == 0 ) {
139+ if (inTimes.front ().num == 0 ) {
140+ d.frameMap [i] = 0 ;
141+ }
142+ else {
143+ vsapi->setError (out, " No valid matches found. Framerate might be too high." );
144+ vsapi->freeNode (d.node );
145+ return ;
146+ }
147+ }
148+ else {
149+ std::deque<timestamp> choicesTemp{ choices };
150+
151+ // Choose the VFR frame(s) with the longest duration.
152+ if (choicesTemp.size () > 1 ) {
153+ double longest = 0 ;
154+ for (unsigned int j = 0 ; j < choicesTemp.size (); j++) {
155+ double start;
156+ if (starttime > choicesTemp[j].start ) {
157+ start = starttime;
158+ }
159+ else {
160+ start = choicesTemp[j].start ;
161+ }
162+ double end;
163+ if (endtime < choicesTemp[j].end ) {
164+ end = endtime;
165+ }
166+ else {
167+ end = choicesTemp[j].end ;
168+ }
169+ if (longest < (end - start)) {
170+ longest = end - start;
171+ }
172+ }
173+ for (unsigned int j = 0 ; j < choicesTemp.size (); j++) {
174+ double start;
175+ if (starttime > choicesTemp[j].start ) {
176+ start = starttime;
177+ }
178+ else {
179+ start = choicesTemp[j].start ;
180+ }
181+ double end;
182+ if (endtime < choicesTemp[j].end ) {
183+ end = endtime;
184+ }
185+ else {
186+ end = choicesTemp[j].end ;
187+ }
188+ if (abs (longest - (end - start)) > static_cast <double >(EPSILON)) {
189+ choicesTemp.erase (choicesTemp.begin () + j);
190+ j--;
191+ }
192+ }
193+ }
194+
195+ // Choose the VFR frame(s) that is closest to the center of the CFR frame.
196+ if (choicesTemp.size () > 1 ) {
197+ double mid = ((endtime - starttime) / 2 ) + starttime;
198+ double shortest = endtime - starttime;
199+ bool found = false ;
200+ timestamp hasMid;
201+ for (unsigned int j = 0 ; j < choicesTemp.size (); j++) {
202+ if (abs (mid - choicesTemp[j].start ) < shortest) {
203+ shortest = abs (mid - choicesTemp[j].start );
204+ }
205+ if (abs (mid - choicesTemp[j].end ) < shortest) {
206+ shortest = abs (mid - choicesTemp[j].end );
207+ }
208+ if (mid > choicesTemp[j].start && abs (mid - choicesTemp[j].start ) > static_cast <double >(EPSILON) && mid < choicesTemp[j].end && abs (mid - choicesTemp[j].end ) > static_cast <double >(EPSILON)) {
209+ hasMid = choicesTemp[j];
210+ found = true ;
211+ break ;
212+ }
213+ }
214+ if (found) {
215+ choicesTemp.clear ();
216+ choicesTemp.push_back (hasMid);
217+ }
218+ else {
219+ for (unsigned int j = 0 ; j < choicesTemp.size (); j++) {
220+ if (abs (shortest - abs (mid - choicesTemp[j].start )) > static_cast <double >(EPSILON) && abs (shortest - abs (mid - choicesTemp[j].end )) > static_cast <double >(EPSILON)) {
221+ choicesTemp.erase (choicesTemp.begin () + j);
222+ j--;
223+ }
224+ }
225+ }
226+ }
227+
228+ // By now, there should be no more than two matches left.
229+
230+ // Choose the frame that hasn't been used for the CFR video yet.
231+ if (choicesTemp.size () > 1 && i != 0 ) {
232+ if (d.frameMap [i - 1 ] == choicesTemp.front ().num ) {
233+ choicesTemp.pop_front ();
234+ }
235+ else if (d.frameMap [i - 1 ] == choicesTemp.back ().num ) {
236+ choicesTemp.pop_back ();
237+ }
238+ }
239+
240+ // If none of them have been used, choose the first one.
241+ d.frameMap [i] = choicesTemp.front ().num ;
242+
243+ // Check if frames were dropped.
244+ if (i != 0 && choicesTemp.front ().num - d.frameMap [i - 1 ] > 1 ) {
245+ drop = true ;
246+ }
247+ }
248+ }
249+
250+ if (!d.drop && drop) {
251+ vsapi->setError (out, " Frames were dropped. Use drop = true if you want to allow frame drops." );
252+ vsapi->freeNode (d.node );
253+ return ;
254+ }
255+
256+ d.vi .numFrames = frames;
257+
258+ VFRToCFRData* data = new VFRToCFRData{ d };
259+
260+ vsapi->createFilter (in, out, " Invert" , VFRToCFRInit, VFRToCFRGetFrame, VFRToCFRFree, fmParallel, 0 , data, core);
261+ }
262+
263+ VS_EXTERNAL_API (void ) VapourSynthPluginInit(VSConfigPlugin configFunc, VSRegisterFunction registerFunc, VSPlugin *plugin) {
264+ configFunc (" blaze.plugin.vfrtocfr" , " vfrtocfr" , " VFR to CFR Video Converter" , VAPOURSYNTH_API_VERSION, 1 , plugin);
265+ registerFunc (" VFRToCFR" , " clip:clip;timecodes:data;fpsnum:int;fpsden:int;drop:int:opt;" , VFRToCFRCreate, nullptr , plugin);
266+ }
0 commit comments